# HG changeset patch # User Goffi # Date 1685699391 -7200 # Node ID 4b842c1fb686f7632805726caea4adf6e4ac7227 # Parent d107484750250011084f5e574f77048dadbd3ee9 refactoring: renamed `sat` package to `libervia.backend` diff -r d10748475025 -r 4b842c1fb686 i18n/fr/LC_MESSAGES/libervia_backend.mo Binary file i18n/fr/LC_MESSAGES/libervia_backend.mo has changed diff -r d10748475025 -r 4b842c1fb686 i18n/fr/LC_MESSAGES/libervia_backend.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/i18n/fr/LC_MESSAGES/libervia_backend.po Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,9225 @@ +# French translations for Libervia. +# Copyright (C) 2021 ORGANIZATION +# This file is distributed under the same license as the Libervia project. +# FIRST AUTHOR , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.0.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-06-15 10:11+0200\n" +"PO-Revision-Date: 2010-03-05 19:24+1100\n" +"Last-Translator: Goffi \n" +"Language: fr\n" +"Language-Team: French \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.9.0\n" + +#: sat/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py:273 +#: sat/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:85 +#: sat/bridge/bridge_constructor/generated/dbus_bridge.py:85 +#: sat/bridge/dbus_bridge.py:747 sat_frontends/bridge/dbus_bridge.py:85 +msgid "" +"D-Bus is not launched, please see README to see instructions on how to " +"launch it" +msgstr "" + +#: sat/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:99 +#: sat/bridge/bridge_constructor/generated/dbus_bridge.py:99 +#: sat_frontends/bridge/dbus_bridge.py:99 +#, fuzzy +msgid "Unknown interface" +msgstr "Type d'action inconnu" + +#: sat/core/sat_main.py:212 +#, fuzzy +msgid "Memory initialised" +msgstr "Le flux XML est initialisé" + +#: sat/core/sat_main.py:219 +msgid "Could not initialize backend: {reason}" +msgstr "" + +#: sat/core/sat_main.py:227 +msgid "Backend is ready" +msgstr "" + +#: sat/core/sat_main.py:238 +msgid "Following profiles will be connected automatically: {profiles}" +msgstr "" + +#: sat/core/sat_main.py:251 +#, fuzzy +msgid "Can't autoconnect profile {profile}: {reason}" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat/core/sat_main.py:321 +msgid "" +"Can't import plugin [{path}]:\n" +"{error}" +msgstr "" + +#: sat/core/sat_main.py:340 +msgid "{type} type must be used with {mode} mode, ignoring plugin" +msgstr "" + +#: sat/core/sat_main.py:349 +msgid "" +"Name conflict for import name [{import_name}], can't import plugin " +"[{name}]" +msgstr "" + +#: sat/core/sat_main.py:385 +msgid "Recommended plugin not found: {}" +msgstr "" + +#: sat/core/sat_main.py:406 +msgid "Can't import plugin {name}: {error}" +msgstr "" + +#: sat/core/sat_main.py:478 +#, fuzzy +msgid "already connected !" +msgstr "Vous n'êtes pas connecté !" + +#: sat/core/sat_main.py:495 +msgid "not connected !" +msgstr "Vous n'êtes pas connecté !" + +#: sat/core/sat_main.py:591 +msgid "Trying to remove reference to a client not referenced" +msgstr "" + +#: sat/core/sat_main.py:604 +msgid "running app" +msgstr "Lancement de l'application" + +#: sat/core/sat_main.py:608 +msgid "stopping app" +msgstr "Arrêt de l'application" + +#: sat/core/sat_main.py:646 +msgid "profile_key must not be empty" +msgstr "" + +#: sat/core/sat_main.py:666 +msgid "Unexpected error: {failure_}" +msgstr "" + +#: sat/core/sat_main.py:921 +msgid "asking connection status for a non-existant profile" +msgstr "demande de l'état de connexion pour un profile qui n'existe pas" + +#: sat/core/sat_main.py:1020 +#, fuzzy, python-format +msgid "subsciption request [%(subs_type)s] for %(jid)s" +msgstr "demande d'inscription [%(type)s] pour %(jid)s" + +#: sat/core/sat_main.py:1162 +msgid "Can't find features for service {service_jid}, ignoring" +msgstr "" + +#: sat/core/sat_main.py:1221 +msgid "Can't retrieve {full_jid} infos, ignoring" +msgstr "" + +#: sat/core/sat_main.py:1292 +msgid "Trying to remove an unknow progress callback" +msgstr "Tentative d'effacement d'une callback de progression inconnue." + +#: sat/core/sat_main.py:1382 +#, fuzzy +msgid "id already registered" +msgstr "Vous êtes maintenant désinscrit" + +#: sat/core/sat_main.py:1424 +#, fuzzy +msgid "trying to launch action with a non-existant profile" +msgstr "Tentative d'ajout d'un contact à un profile inexistant" + +#: sat/core/sat_main.py:1520 +#, fuzzy +msgid "A menu with the same path and type already exists" +msgstr "Ce nom de profile existe déjà" + +#: sat/core/sat_main.py:1619 +#, fuzzy +msgid "help_string" +msgstr "enregistrement" + +#: sat/core/xmpp.py:196 +#, fuzzy +msgid "Can't parse port value, using default value" +msgstr "Pas de modèle de paramètres, utilisation du modèle par défaut" + +#: sat/core/xmpp.py:223 +msgid "We'll use the stable resource {resource}" +msgstr "" + +#: sat/core/xmpp.py:255 +msgid "setting plugins parents" +msgstr "Configuration des parents des extensions" + +#: sat/core/xmpp.py:275 +#, fuzzy +msgid "Plugins initialisation error" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/core/xmpp.py:297 +msgid "Error while disconnecting: {}" +msgstr "" + +#: sat/core/xmpp.py:301 +#, fuzzy +msgid "{profile} identified" +msgstr "Aucun profile sélectionné" + +#: sat/core/xmpp.py:309 +msgid "XML stream is initialized" +msgstr "Le flux XML est initialisé" + +#: sat/core/xmpp.py:317 +#, fuzzy, python-format +msgid "********** [{profile}] CONNECTED **********" +msgstr "********** [%s] CONNECTÉ **********" + +#: sat/core/xmpp.py:343 +#, python-format +msgid "ERROR: XMPP connection failed for profile '%(profile)s': %(reason)sprofile" +msgstr "" + +#: sat/core/xmpp.py:398 +msgid "stopping connection because of network disabled" +msgstr "" + +#: sat/core/xmpp.py:421 +msgid "network is available, trying to connect" +msgstr "" + +#: sat/core/xmpp.py:445 +#, fuzzy, python-format +msgid "********** [{profile}] DISCONNECTED **********" +msgstr "********** [%s] CONNECTÉ **********" + +#: sat/core/xmpp.py:464 +msgid "" +"Your server certificate is not valid (its identity can't be checked).\n" +"\n" +"This should never happen and may indicate that somebody is trying to spy " +"on you.\n" +"Please contact your server administrator." +msgstr "" + +#: sat/core/xmpp.py:515 +#, fuzzy +msgid "Disconnecting..." +msgstr "Déconnexion..." + +#: sat/core/xmpp.py:688 +#, fuzzy, python-format +msgid "Sending message (type {type}, to {to})" +msgstr "Envoi du message jabber à %s" + +#: sat/core/xmpp.py:696 +msgid "" +"Triggers, storage and echo have been inhibited by the 'send_only' " +"parameter" +msgstr "" + +#: sat/core/xmpp.py:762 +#, fuzzy, python-format +msgid "No message found" +msgstr "message reçu de: %s" + +#: sat/core/xmpp.py:814 +msgid "invalid data used for host: {data}" +msgstr "" + +#: sat/core/xmpp.py:839 +msgid "" +"Certificate validation is deactivated, this is unsecure and somebody may " +"be spying on you. If you have no good reason to disable certificate " +"validation, please activate \"Check certificate\" in your settings in " +"\"Connection\" tab." +msgstr "" + +#: sat/core/xmpp.py:843 +msgid "Security notice" +msgstr "" + +#: sat/core/xmpp.py:978 +msgid "The requested entry point ({entry_point}) is not available" +msgstr "" + +#: sat/core/xmpp.py:1016 +msgid "" +"Plugin {current_name} is needed for {entry_name}, but it doesn't handle " +"component mode" +msgstr "" + +#: sat/core/xmpp.py:1024 +msgid "invalid plugin mode" +msgstr "" + +#: sat/core/xmpp.py:1128 +msgid "parseMessage used with a non stanza, ignoring: {xml}" +msgstr "" + +#: sat/core/xmpp.py:1140 +msgid "received with a wrong namespace: {xml}" +msgstr "" + +#: sat/core/xmpp.py:1226 +#, fuzzy, python-format +msgid "got message from: {from_}" +msgstr "message reçu de: %s" + +#: sat/core/xmpp.py:1341 +msgid "There's no subscription between you and [{}]!" +msgstr "" + +#: sat/core/xmpp.py:1346 +msgid "You are not subscribed to [{}]!" +msgstr "" + +#: sat/core/xmpp.py:1348 +msgid "[{}] is not subscribed to you!" +msgstr "" + +#: sat/core/xmpp.py:1384 +msgid "our server support roster versioning, we use it" +msgstr "" + +#: sat/core/xmpp.py:1390 +msgid "no roster in cache, we start fresh" +msgstr "" + +#: sat/core/xmpp.py:1394 +msgid "We have roster v{version} in cache" +msgstr "" + +#: sat/core/xmpp.py:1405 +msgid "our server doesn't support roster versioning" +msgstr "" + +#: sat/core/xmpp.py:1462 +msgid "adding {entity} to roster" +msgstr "" + +#: sat/core/xmpp.py:1486 +#, fuzzy, python-format +msgid "removing {entity} from roster" +msgstr "supppression du contact %s" + +#: sat/core/xmpp.py:1640 +#, 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/core/xmpp.py:1724 +#, fuzzy +msgid "sending automatic \"from\" subscription request" +msgstr "envoi automatique de la demande d'inscription \"to\"" + +#: sat/core/xmpp.py:1732 +#, python-format +msgid "subscription approved for [%s]" +msgstr "inscription approuvée pour [%s]" + +#: sat/core/xmpp.py:1736 +#, fuzzy, python-format +msgid "unsubscription confirmed for [%s]" +msgstr "demande de désinscription pour [%s]" + +#: sat/core/xmpp.py:1741 +#, fuzzy, python-format +msgid "subscription request from [%s]" +msgstr "inscription approuvée pour [%s]" + +#: sat/core/xmpp.py:1747 +#, fuzzy +msgid "sending automatic subscription acceptance" +msgstr "envoi automatique de la demande d'inscription \"to\"" + +#: sat/core/xmpp.py:1759 +#, python-format +msgid "unsubscription asked for [%s]" +msgstr "demande de désinscription pour [%s]" + +#: sat/core/xmpp.py:1763 +#, fuzzy +msgid "automatic contact deletion" +msgstr "Sélection du contrat" + +#: sat/memory/cache.py:69 +msgid "Can't read metadata file at {path}" +msgstr "" + +#: sat/memory/cache.py:80 +msgid "Invalid cache metadata at {path}" +msgstr "" + +#: sat/memory/cache.py:87 +msgid "cache {cache_file!r} references an inexisting file: {filepath!r}" +msgstr "" + +#: sat/memory/cache.py:102 +msgid "following file is missing while purging cache: {path}" +msgstr "" + +#: sat/memory/cache.py:200 +msgid "missing filename for cache {uid!r}" +msgstr "" + +#: sat/memory/cache.py:207 +msgid "missing file referenced in cache {uid!r}: {filename}" +msgstr "" + +#: sat/memory/disco.py:95 +msgid "" +"no feature/identity found in disco element (hash: {cap_hash}), ignoring: " +"{xml}" +msgstr "" + +#: sat/memory/disco.py:274 +#, python-format +msgid "Error while requesting [%(jid)s]: %(error)s" +msgstr "" + +#: sat/memory/disco.py:338 +msgid "received an item without jid" +msgstr "" + +#: sat/memory/disco.py:410 +msgid "Capability hash generated: [{cap_hash}]" +msgstr "" + +#: sat/memory/disco.py:459 +msgid "invalid item (no jid)" +msgstr "" + +#: sat/memory/encryption.py:71 +msgid "Could not restart {namespace!r} encryption with {entity}: {err}" +msgstr "" + +#: sat/memory/encryption.py:74 +msgid "encryption sessions restored" +msgstr "" + +#: sat/memory/encryption.py:116 +msgid "Encryption plugin registered: {name}" +msgstr "" + +#: sat/memory/encryption.py:127 +msgid "Can't find requested encryption plugin: {namespace}" +msgstr "" + +#: sat/memory/encryption.py:148 +msgid "Can't find a plugin with the name \"{name}\"." +msgstr "" + +#: sat/memory/encryption.py:213 +msgid "No encryption plugin is registered, an encryption session can't be started" +msgstr "" + +#: sat/memory/encryption.py:226 +msgid "Session with {bare_jid} is already encrypted with {name}. Nothing to do." +msgstr "" + +#: sat/memory/encryption.py:237 +msgid "" +"Session with {bare_jid} is already encrypted with {name}. Please stop " +"encryption session before changing algorithm." +msgstr "" + +#: sat/memory/encryption.py:249 +msgid "No resource found for {destinee}, can't encrypt with {name}" +msgstr "" + +#: sat/memory/encryption.py:251 +msgid "No resource specified to encrypt with {name}, using {destinee}." +msgstr "" + +#: sat/memory/encryption.py:257 +msgid "{name} encryption must be used with bare jids." +msgstr "" + +#: sat/memory/encryption.py:261 +msgid "Encryption session has been set for {entity_jid} with {encryption_name}" +msgstr "" + +#: sat/memory/encryption.py:268 +msgid "" +"Encryption session started: your messages with {destinee} are now end to " +"end encrypted using {name} algorithm." +msgstr "" + +#: sat/memory/encryption.py:273 +msgid "Message are encrypted only for {nb_devices} device(s): {devices_list}." +msgstr "" + +#: sat/memory/encryption.py:291 +msgid "There is no encryption session with this entity." +msgstr "" + +#: sat/memory/encryption.py:295 +msgid "" +"The encryption session is not run with the expected plugin: encrypted " +"with {current_name} and was expecting {expected_name}" +msgstr "" + +#: sat/memory/encryption.py:304 +msgid "" +"There is a session for the whole entity (i.e. all devices of the entity)," +" not a directed one. Please use bare jid if you want to stop the whole " +"encryption with this entity." +msgstr "" + +#: sat/memory/encryption.py:312 +msgid "There is no directed session with this entity." +msgstr "" + +#: sat/memory/encryption.py:327 +msgid "encryption session stopped with entity {entity}" +msgstr "" + +#: sat/memory/encryption.py:335 +msgid "" +"Encryption session finished: your messages with {destinee} are NOT end to" +" end encrypted anymore.\n" +"Your server administrators or {destinee} server administrators will be " +"able to read them." +msgstr "" + +#: sat/memory/encryption.py:389 sat/memory/encryption.py:397 +#: sat/memory/encryption.py:404 +#, fuzzy +msgid "Encryption" +msgstr "Connexion..." + +#: sat/memory/encryption.py:389 +msgid "unencrypted (plain text)" +msgstr "" + +#: sat/memory/encryption.py:392 +msgid "End encrypted session" +msgstr "" + +#: sat/memory/encryption.py:400 +msgid "Start {name} session" +msgstr "" + +#: sat/memory/encryption.py:404 +msgid "⛨ {name} trust" +msgstr "" + +#: sat/memory/encryption.py:407 +msgid "Manage {name} trust" +msgstr "" + +#: sat/memory/encryption.py:470 +msgid "Starting e2e session with {peer_jid} as we receive encrypted messages" +msgstr "" + +#: sat/memory/memory.py:230 +msgid "Memory manager init" +msgstr "Initialisation du gestionnaire de mémoire" + +#: sat/memory/memory.py:249 +#, fuzzy +msgid "Loading default params template" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat/memory/memory.py:281 +#, python-format +msgid "Parameters loaded from file: %s" +msgstr "" + +#: sat/memory/memory.py:284 +#, fuzzy, python-format +msgid "Can't load parameters from file: %s" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat/memory/memory.py:299 +#, fuzzy, python-format +msgid "Parameters saved to file: %s" +msgstr "Échec de la désinscription: %s" + +#: sat/memory/memory.py:302 +#, fuzzy, python-format +msgid "Can't save parameters to file: %s" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat/memory/memory.py:404 +msgid "Authentication failure of profile {profile}" +msgstr "" + +#: sat/memory/memory.py:431 +#, fuzzy, python-format +msgid "[%s] Profile session purge" +msgstr "Ce profile n'est pas utilisé" + +#: sat/memory/memory.py:437 +#, python-format +msgid "Trying to purge roster status cache for a profile not in memory: [%s]" +msgstr "" + +#: sat/memory/memory.py:451 +msgid "requesting no profiles at all" +msgstr "" + +#: sat/memory/memory.py:508 +msgid "Can't find component {component} entry point" +msgstr "" + +#: sat/memory/memory.py:996 +msgid "Need a bare jid to delete all resources" +msgstr "" + +#: sat/memory/memory.py:1028 +#, python-format +msgid "Trying to encrypt a value for %s while the personal key is undefined!" +msgstr "" + +#: sat/memory/memory.py:1048 +#, python-format +msgid "Trying to decrypt a value for %s while the personal key is undefined!" +msgstr "" + +#: sat/memory/memory.py:1069 +#, python-format +msgid "Personal data (%(ns)s, %(key)s) has been successfuly encrypted" +msgstr "" + +#: sat/memory/memory.py:1097 +msgid "Asking waiting subscriptions for a non-existant profile" +msgstr "Demande des inscriptions en attente pour un profile inexistant" + +#: sat/memory/memory.py:1218 +msgid "invalid permission" +msgstr "" + +#: sat/memory/memory.py:1249 +#, fuzzy +msgid "unknown access type: {type}" +msgstr "Type d'action inconnu" + +#: sat/memory/memory.py:1284 +msgid "You can't use path and parent at the same time" +msgstr "" + +#: sat/memory/memory.py:1288 +msgid "\"..\" or \".\" can't be used in path" +msgstr "" + +#: sat/memory/memory.py:1307 +msgid "Several directories found, this should not happen" +msgstr "" + +#: sat/memory/memory.py:1766 +msgid "Can't delete directory, it is not empty" +msgstr "" + +#: sat/memory/memory.py:1778 +msgid "deleting file {name} with hash {file_hash}" +msgstr "" + +#: sat/memory/memory.py:1787 +msgid "no reference left to {file_path}, deleting" +msgstr "" + +#: sat/memory/params.py:85 sat_frontends/primitivus/base.py:533 +msgid "General" +msgstr "Général" + +#: sat/memory/params.py:86 +#, fuzzy +msgid "Connection" +msgstr "Connexion..." + +#: sat/memory/params.py:88 +msgid "Chat history limit" +msgstr "" + +#: sat/memory/params.py:90 +msgid "Show offline contacts" +msgstr "" + +#: sat/memory/params.py:92 +msgid "Show empty groups" +msgstr "" + +#: sat/memory/params.py:95 +#, fuzzy +msgid "Connect on backend startup" +msgstr "Connexion au démarrage des frontends" + +#: sat/memory/params.py:96 +#, fuzzy +msgid "Connect on frontend startup" +msgstr "Connexion au démarrage des frontends" + +#: sat/memory/params.py:97 +#, fuzzy +msgid "Disconnect on frontend closure" +msgstr "Déconnexion à la fermeture des frontends" + +#: sat/memory/params.py:98 +msgid "Check certificate (don't uncheck if unsure)" +msgstr "" + +#: sat/memory/params.py:163 +#, fuzzy, python-format +msgid "Trying to purge cache of a profile not in memory: [%s]" +msgstr "Tentative d'appel d'un profile inconnue" + +#: sat/memory/params.py:188 +#, fuzzy +msgid "The profile name already exists" +msgstr "Ce nom de profile existe déjà" + +#: sat/memory/params.py:203 +#, fuzzy +msgid "Trying to delete an unknown profile" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat/memory/params.py:209 +#, fuzzy +msgid "Trying to delete a connected profile" +msgstr "Tentative de suppression d'un contact pour un profile inexistant" + +#: sat/memory/params.py:228 +msgid "No default profile, returning first one" +msgstr "Pas de profile par défaut, envoi du premier" + +#: sat/memory/params.py:234 +#, fuzzy +msgid "No profile exist yet" +msgstr "Aucun profile sélectionné" + +#: sat/memory/params.py:244 +#, fuzzy, python-format +msgid "Trying to access an unknown profile (%s)" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat/memory/params.py:338 +msgid "Trying to register frontends parameters with no specified app: aborted" +msgstr "" + +#: sat/memory/params.py:347 +#, python-format +msgid "Trying to register twice frontends parameters for %(app)s: abortedapp" +msgstr "" + +#: sat/memory/params.py:363 +#, 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" + +#: sat/memory/params.py:385 sat/memory/params.py:563 sat/memory/params.py:624 +#, 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 !" + +#: sat/memory/params.py:440 +#, python-format +msgid "" +"Unset parameter (%(cat)s, %(param)s) of type list will use the default " +"option '%(value)s'" +msgstr "" + +#: sat/memory/params.py:448 +#, python-format +msgid "Parameter (%(cat)s, %(param)s) of type list has no default option!" +msgstr "" + +#: sat/memory/params.py:455 +#, python-format +msgid "" +"Parameter (%(cat)s, %(param)s) of type list has more than one default " +"option!" +msgstr "" + +#: sat/memory/params.py:585 +msgid "Requesting a param for an non-existant profile" +msgstr "Demande d'un paramètre pour un profile inconnu" + +#: sat/memory/params.py:589 +#, fuzzy +msgid "Requesting synchronous param for not connected profile" +msgstr "Demande d'un paramètre pour un profile inconnu" + +#: sat/memory/params.py:633 +#, python-format +msgid "" +"Trying to get parameter '%(param)s' in category '%(cat)s' without " +"authorization!!!param" +msgstr "" + +#: sat/memory/params.py:649 +#, fuzzy +msgid "Requesting a param for a non-existant profile" +msgstr "Demande d'un paramètre pour un profile inconnu" + +#: sat/memory/params.py:962 +#, fuzzy +msgid "Trying to set parameter for an unknown profile" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat/memory/params.py:968 +#, python-format +msgid "Requesting an unknown parameter (%(category)s/%(name)s)" +msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" + +#: sat/memory/params.py:974 +msgid "" +"{profile!r} is trying to set parameter {name!r} in category {category!r} " +"without authorization!!!" +msgstr "" + +#: sat/memory/params.py:992 +msgid "" +"Trying to set parameter {name} in category {category} withan non-integer " +"value" +msgstr "" + +#: sat/memory/params.py:1011 +#, fuzzy, python-format +msgid "Setting parameter (%(category)s, %(name)s) = %(value)s" +msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" + +#: sat/memory/params.py:1043 +msgid "Trying to encrypt a password while the personal key is undefined!" +msgstr "" + +#: sat/memory/persistent.py:45 +msgid "PersistentDict can't be used before memory initialisation" +msgstr "" + +#: sat/memory/persistent.py:175 +msgid "Calling load on LazyPersistentBinaryDict while it's not needed" +msgstr "" + +#: sat/memory/sqlite.py:163 +msgid "" +"too many db tries, we abandon! Error message: {msg}\n" +"query was {query}" +msgstr "" + +#: sat/memory/sqlite.py:166 +msgid "exception while running query, retrying ({try_}): {msg}" +msgstr "" + +#: sat/memory/sqlite.py:188 +msgid "" +"too many interaction tries, we abandon! Error message: {msg}\n" +"interaction method was: {interaction}\n" +"interaction arguments were: {args}" +msgstr "" + +#: sat/memory/sqlite.py:191 +msgid "exception while running interaction, retrying ({try_}): {msg}" +msgstr "" + +#: sat/memory/sqlite.py:210 +msgid "Connecting database" +msgstr "" + +#: sat/memory/sqlite.py:223 +#, fuzzy +msgid "The database is new, creating the tables" +msgstr "Ce nom de profile existe déjà" + +#: sat/memory/sqlite.py:337 +#, fuzzy, python-format +msgid "Can't delete profile [%s]" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat/memory/sqlite.py:354 +#, fuzzy, python-format +msgid "Profile [%s] deleted" +msgstr "Aucun profile sélectionné" + +#: sat/memory/sqlite.py:370 +#, fuzzy +msgid "loading general parameters from database" +msgstr "Impossible de charger les paramètres généraux !" + +#: sat/memory/sqlite.py:385 +#, fuzzy +msgid "loading individual parameters from database" +msgstr "Impossible de charger les paramètres individuels !" + +#: sat/memory/sqlite.py:426 +#, fuzzy, python-format +msgid "Can't set general parameter (%(category)s/%(name)s) in databasecategory" +msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" + +#: sat/memory/sqlite.py:439 +#, fuzzy, python-format +msgid "" +"Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] " +"in databasecategory" +msgstr "" +"Impossible de déterminer la valeur par défaut pour " +"[%(category)s/%(name)s]: %(reason)s" + +#: sat/memory/sqlite.py:459 +msgid "Can't save following {key} in history (uid: {uid}, lang:{lang}): {value}" +msgstr "" + +#: sat/memory/sqlite.py:473 +msgid "" +"Can't save following thread in history (uid: {uid}): thread: {thread}), " +"parent:{parent}" +msgstr "" + +#: sat/memory/sqlite.py:498 +msgid "" +"Can't save following message in history: from [{from_jid}] to [{to_jid}] " +"(uid: {uid})" +msgstr "" + +#: sat/memory/sqlite.py:701 +msgid "" +"Can't {operation} data in database for namespace " +"{namespace}{and_key}{for_profile}: {msg}" +msgstr "" + +#: sat/memory/sqlite.py:752 +msgid "" +"getting {type}{binary} private values from database for namespace " +"{namespace}{keys}" +msgstr "" + +#: sat/memory/sqlite.py:986 +msgid "Can't save file metadata for [{profile}]: {reason}" +msgstr "" + +#: sat/memory/sqlite.py:1025 +msgid "table not updated, probably due to race condition, trying again ({tries})" +msgstr "" + +#: sat/memory/sqlite.py:1027 +msgid "Can't update file table" +msgstr "" + +#: sat/memory/sqlite.py:1132 +msgid "" +"Your local schema is up-to-date, but database versions mismatch, fixing " +"it..." +msgstr "" + +#: sat/memory/sqlite.py:1142 +msgid "" +"There is a schema mismatch, but as we are on a dev version, database will" +" be updated" +msgstr "" + +#: sat/memory/sqlite.py:1146 +msgid "" +"schema version is up-to-date, but local schema differ from expected " +"current schema" +msgstr "" + +#: sat/memory/sqlite.py:1149 +#, python-format +msgid "" +"Here are the commands that should fix the situation, use at your own risk" +" (do a backup before modifying database), you can go to SàT's MUC room at" +" sat@chat.jabberfr.org for help\n" +"### SQL###\n" +"%s\n" +"### END SQL ###\n" +msgstr "" + +#: sat/memory/sqlite.py:1153 +msgid "" +"You database version is higher than the one used in this SàT version, are" +" you using several version at the same time? We can't run SàT with this " +"database." +msgstr "" + +#: sat/memory/sqlite.py:1161 +msgid "" +"Database content needs a specific processing, local database will be " +"updated" +msgstr "" + +#: sat/memory/sqlite.py:1163 +msgid "Database schema has changed, local database will be updated" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:91 +#, fuzzy +msgid "Add D-Bus management to Ad-Hoc commands" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_adhoc_dbus.py:98 +#, fuzzy +msgid "plugin Ad-Hoc D-Bus initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_adhoc_dbus.py:127 +msgid "Media Players" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:255 +#, fuzzy +msgid "Command selection" +msgstr "Sélection du contrat" + +#: sat/plugins/plugin_adhoc_dbus.py:298 +#, fuzzy +msgid "Updated" +msgstr "mise à jour de %s" + +#: sat/plugins/plugin_adhoc_dbus.py:302 +msgid "Command sent" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:367 +msgid "Can't retrieve remote controllers on {device_jid}: {reason}" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:405 +#, fuzzy +msgid "No media player found." +msgstr "Aucune donnée trouvée" + +#: sat/plugins/plugin_adhoc_dbus.py:409 sat/plugins/plugin_adhoc_dbus.py:451 +#, fuzzy +msgid "Media Player Selection" +msgstr "Sélection du contrat" + +#: sat/plugins/plugin_adhoc_dbus.py:414 +msgid "Ignoring MPRIS bus without suffix" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:428 +msgid "missing media_player value" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:431 +msgid "" +"Media player ad-hoc command trying to use non MPRIS bus. Hack attempt? " +"Refused bus: {bus_name}" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:434 +msgid "Invalid player name." +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:440 +msgid "Can't get D-Bus proxy: {reason}" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:441 +msgid "Media player is not available anymore" +msgstr "" + +#: sat/plugins/plugin_adhoc_dbus.py:460 +msgid "Can't retrieve attribute {name}: {reason}" +msgstr "" + +#: sat/plugins/plugin_blog_import.py:45 +msgid "" +"Blog import management:\n" +"This plugin manage the different blog importers which can register to it," +" and handle generic importing tasks." +msgstr "" + +#: sat/plugins/plugin_blog_import.py:64 +#, fuzzy +msgid "plugin Blog Import initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:58 +msgid "Blog importer for Dokuwiki blog engine." +msgstr "" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:61 +msgid "import posts from Dokuwiki blog engine" +msgstr "" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:63 +msgid "" +"This importer handle Dokuwiki blog engine.\n" +"\n" +"To use it, you need an admin access to a running Dokuwiki website\n" +"(local or on the Internet). The importer retrieves the data using\n" +"the XMLRPC Dokuwiki API.\n" +"\n" +"You can specify a namespace (that could be a namespace directory\n" +"or a single post) or leave it empty to use the root namespace \"/\"\n" +"and import all the posts.\n" +"\n" +"You can specify a new media repository to modify the internal\n" +"media links and make them point to the URL of your choice, but\n" +"note that the upload is not done automatically: a temporary\n" +"directory will be created on your local drive and you will\n" +"need to upload it yourself to your repository via SSH or FTP.\n" +"\n" +"Following options are recognized:\n" +"\n" +"location: DokuWiki site URL\n" +"user: DokuWiki admin user\n" +"passwd: DokuWiki admin password\n" +"namespace: DokuWiki namespace to import (default: root namespace \"/\")\n" +"media_repo: URL to the new remote media repository (default: none)\n" +"limit: maximal number of posts to import (default: 100)\n" +"\n" +"Example of usage (with jp frontend):\n" +"\n" +"jp import dokuwiki -p dave --pwd xxxxxx --connect\n" +" http://127.0.1.1 -o user souliane -o passwd qwertz\n" +" -o namespace public:2015:10\n" +" -o media_repo http://media.diekulturvermittlung.at\n" +"\n" +"This retrieves the 100 last blog posts from http://127.0.1.1 that\n" +"are inside the namespace \"public:2015:10\" using the Dokuwiki user\n" +"\"souliane\", and it imports them to sat profile dave's microblog node.\n" +"Internal Dokuwiki media that were hosted on http://127.0.1.1 are now\n" +"pointing to http://media.diekulturvermittlung.at.\n" +msgstr "" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:351 +#, fuzzy +msgid "plugin Dokuwiki Import initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:383 +msgid "" +"DokuWiki media files will be *downloaded* to {temp_dir} - to finish the " +"import you have to upload them *manually* to {media_repo}" +msgstr "" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:389 +msgid "" +"DokuWiki media files will be *uploaded* to the XMPP server. Hyperlinks to" +" these media may not been updated though." +msgstr "" + +#: sat/plugins/plugin_blog_import_dokuwiki.py:393 +msgid "" +"DokuWiki media files will *stay* on {location} - some of them may be " +"protected by DokuWiki ACL and will not be accessible." +msgstr "" + +#: sat/plugins/plugin_blog_import_dotclear.py:42 +msgid "Blog importer for Dotclear blog engine." +msgstr "" + +#: sat/plugins/plugin_blog_import_dotclear.py:45 +msgid "import posts from Dotclear blog engine" +msgstr "" + +#: sat/plugins/plugin_blog_import_dotclear.py:47 +msgid "" +"This importer handle Dotclear blog engine.\n" +"\n" +"To use it, you'll need to export your blog to a flat file.\n" +"You must go in your admin interface and select Plugins/Maintenance then " +"Backup.\n" +"Export only one blog if you have many, i.e. select \"Download database of" +" current blog\"\n" +"Depending on your configuration, your may need to use Import/Export " +"plugin and export as a flat file.\n" +"\n" +"location: you must use the absolute path to your backup for the location " +"parameter\n" +msgstr "" + +#: sat/plugins/plugin_blog_import_dotclear.py:266 +#, fuzzy +msgid "plugin Dotclear Import initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_comp_file_sharing.py:69 +msgid "Component hosting and sharing files" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:79 +msgid "" +"You are over quota, your maximum allowed size is {quota} and you are " +"already using {used_space}, you can't upload {file_size} more." +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:350 +#, fuzzy +msgid "File Sharing initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_comp_file_sharing.py:431 +#: sat/plugins/plugin_comp_file_sharing_management.py:422 +msgid "Can't create thumbnail: {reason}" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:454 +msgid "Reusing already generated hash" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:485 +msgid "Can't get thumbnail for {final_path}: {e}" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:574 +msgid "{peer_jid} is trying to access an unauthorized file: {name}" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing.py:582 +msgid "no matching file found ({file_data})" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:43 +msgid "" +"Experimental handling of file management for file sharing. This plugins " +"allows to change permissions of stored files/directories or remove them." +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:72 +#, fuzzy +msgid "File Sharing Management plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_comp_file_sharing_management.py:185 +#, fuzzy +msgid "file not found" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_comp_file_sharing_management.py:187 +#: sat/plugins/plugin_comp_file_sharing_management.py:192 +#: sat/plugins/plugin_comp_file_sharing_management.py:474 +msgid "forbidden" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:191 +msgid "Only owner can manage files" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:258 +msgid "Please select permissions for this directory" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:260 +msgid "Please select permissions for this file" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:305 +msgid "Can't use read_allowed values: {reason}" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:332 +msgid "management session done" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:358 +msgid "" +"Are you sure to delete directory {name} and all files and directories " +"under it?" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:362 +#, fuzzy, python-format +msgid "Are you sure to delete file {name}?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat/plugins/plugin_comp_file_sharing_management.py:387 +#, fuzzy, python-format +msgid "file deleted" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_comp_file_sharing_management.py:465 +msgid "thumbnails generated" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:481 +msgid "You are currently using {size_used} on {size_quota}" +msgstr "" + +#: sat/plugins/plugin_comp_file_sharing_management.py:484 +msgid "unlimited quota" +msgstr "" + +#: sat/plugins/plugin_dbg_manhole.py:39 +msgid "Debug plugin to have a telnet server" +msgstr "" + +#: sat/plugins/plugin_dbg_manhole.py:53 +msgid "" +"/!\\ Manhole debug server activated, be sure to not use it in production," +" this is dangerous /!\\" +msgstr "" + +#: sat/plugins/plugin_dbg_manhole.py:55 +msgid "You can connect to manhole server using telnet on port {port}" +msgstr "" + +#: sat/plugins/plugin_exp_command_export.py:39 +#, fuzzy +msgid "Implementation of command export" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_exp_command_export.py:92 +#, fuzzy +msgid "Plugin command export initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_events.py:50 +msgid "Experimental implementation of XMPP events management" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:60 +#, fuzzy +msgid "Event plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_events.py:177 +msgid "no src found for image" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:187 +#, fuzzy +msgid "no {uri_type} element found!" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_exp_events.py:189 +msgid "incomplete {uri_type} element" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:191 +msgid "bad {uri_type} element" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:231 +#, fuzzy +msgid "No event element has been found" +msgstr "Aucune donnée trouvée" + +#: sat/plugins/plugin_exp_events.py:233 +msgid "No event with this id has been found" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:290 +msgid "event_id must be set" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:333 +msgid "The given URI is not valid: {uri}" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:354 +#: sat/plugins/plugin_exp_list_of_interest.py:100 +#, fuzzy +msgid "requested node already exists" +msgstr "Ce nom de profile existe déjà" + +#: sat/plugins/plugin_exp_events.py:373 +msgid "missing node" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:426 +msgid "No event found in item {item_id}, ignoring" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:519 +msgid "no data found for {item_id} (service: {service}, node: {node})" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:542 sat/plugins/plugin_exp_events.py:623 +msgid "\"XEP-0277\" (blog) plugin is needed for this feature" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:548 +msgid "got event data" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:579 +msgid "affiliation set on blog and comments nodes" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:619 +msgid "\"Invitations\" plugin is needed for this feature" +msgstr "" + +#: sat/plugins/plugin_exp_events.py:632 +#, fuzzy +msgid "invitation created" +msgstr "Connexion..." + +#: sat/plugins/plugin_exp_invitation.py:44 +#, fuzzy +msgid "Experimental handling of invitations" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_exp_invitation.py:57 +#, fuzzy +msgid "Invitation plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_exp_invitation.py:251 +msgid "Can't get item linked with invitation: {reason}" +msgstr "" + +#: sat/plugins/plugin_exp_invitation.py:256 +msgid "Invitation was linking to a non existing item" +msgstr "" + +#: sat/plugins/plugin_exp_invitation.py:262 +msgid "Can't retrieve namespace of invitation: {reason}" +msgstr "" + +#: sat/plugins/plugin_exp_invitation.py:281 +msgid "Bad invitation, ignoring" +msgstr "" + +#: sat/plugins/plugin_exp_invitation.py:321 +msgid "No handler for namespace \"{namespace}\", invitation ignored" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_file.py:39 +msgid "Experimental handling of invitations for file sharing" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_file.py:46 +#, fuzzy +msgid "File Sharing Invitation plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_exp_invitation_file.py:85 +#: sat/plugins/plugin_exp_invitation_file.py:92 +msgid "file sharing" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_file.py:87 +msgid "photo album" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_file.py:93 +msgid "" +"{profile} has received an invitation for a files repository " +"({type_human}) with namespace {sharing_ns!r} at path [{path}]" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_pubsub.py:42 +msgid "Invitations for pubsub based features" +msgstr "" + +#: sat/plugins/plugin_exp_invitation_pubsub.py:49 +#, fuzzy +msgid "Pubsub Invitation plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_jingle_stream.py:52 +msgid "Jingle Stream plugin" +msgstr "" + +#: sat/plugins/plugin_exp_jingle_stream.py:55 +#, fuzzy, python-format +msgid "{peer} wants to send you a stream, do you accept ?" +msgstr "" +"Le contact %(jid)s veut vous envoyer le fichier %(filename)s\n" +"Êtes vous d'accord ?" + +#: sat/plugins/plugin_exp_jingle_stream.py:56 +#, fuzzy +msgid "Stream Request" +msgstr "Gestion des paramètres" + +#: sat/plugins/plugin_exp_jingle_stream.py:123 +msgid "stream can't be used with multiple consumers" +msgstr "" + +#: sat/plugins/plugin_exp_jingle_stream.py:170 +#, fuzzy +msgid "No client connected, can't send data" +msgstr "Connexion du client SOCKS 5 démarrée" + +#: sat/plugins/plugin_exp_jingle_stream.py:180 +#, fuzzy +msgid "Plugin Stream initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_jingle_stream.py:270 +msgid "given port is invalid" +msgstr "" + +#: sat/plugins/plugin_exp_lang_detect.py:45 +msgid "Detect and set message language when unknown" +msgstr "" + +#: sat/plugins/plugin_exp_lang_detect.py:48 +#: sat/plugins/plugin_misc_watched.py:43 sat/plugins/plugin_xep_0249.py:73 +msgid "Misc" +msgstr "Divers" + +#: sat/plugins/plugin_exp_lang_detect.py:50 +msgid "language detection" +msgstr "" + +#: sat/plugins/plugin_exp_lang_detect.py:66 +#, fuzzy +msgid "Language detection plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_exp_list_of_interest.py:46 +msgid "Experimental handling of interesting XMPP locations" +msgstr "" + +#: sat/plugins/plugin_exp_list_of_interest.py:56 +#, fuzzy +msgid "List of Interest plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_exp_list_of_interest.py:287 +msgid "Missing interest element: {xml}" +msgstr "" + +#: sat/plugins/plugin_exp_parrot.py:40 +msgid "Implementation of parrot mode (repeat messages between 2 entities)" +msgstr "" + +#: sat/plugins/plugin_exp_parrot.py:56 +#, fuzzy +msgid "Plugin Parrot initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_parrot.py:63 sat/plugins/plugin_xep_0045.py:150 +#: sat/plugins/plugin_xep_0048.py:102 sat/plugins/plugin_xep_0092.py:61 +#: sat/plugins/plugin_xep_0199.py:56 sat/plugins/plugin_xep_0249.py:95 +#: sat/plugins/plugin_xep_0384.py:476 +msgid "Text commands not available" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_admin.py:40 +msgid "" +"\\Implementation of Pubsub Administrator\n" +"This allows a pubsub administrator to overwrite completly items, " +"including publisher.\n" +"Specially useful when importing a node." +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:40 +msgid "Experimental plugin to launch on action on Pubsub notifications" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:56 +#, fuzzy +msgid "PubSub Hook initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_exp_pubsub_hook.py:93 +msgid "node manager already set for {node}" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:101 +msgid "node manager installed on {node}" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:107 +#, fuzzy +msgid "trying to remove a {node} without hook" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat/plugins/plugin_exp_pubsub_hook.py:112 +msgid "hook removed" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:114 +msgid "node still needed for an other hook" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:119 +msgid "{hook_type} is not handled" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:123 +#: sat/plugins/plugin_exp_pubsub_hook.py:167 +msgid "{hook_type} hook type not implemented yet" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:139 +msgid "{persistent} hook installed on {node} for {profile}" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:140 +msgid "persistent" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:140 +msgid "temporary" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:173 +msgid "Can't load Pubsub hook at node {node}, it will be removed: {reason}" +msgstr "" + +#: sat/plugins/plugin_exp_pubsub_hook.py:185 +msgid "Error while running Pubsub hook for node {node}: {msg}" +msgstr "" + +#: sat/plugins/plugin_import.py:41 +msgid "Generic import plugin, base for specialized importers" +msgstr "" + +#: sat/plugins/plugin_import.py:49 +#, fuzzy +msgid "plugin Import initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_import.py:67 +msgid "initializing {name} import handler" +msgstr "" + +#: sat/plugins/plugin_import.py:158 +msgid "invalid json option: {option}" +msgstr "" + +#: sat/plugins/plugin_import.py:296 +msgid "uploading subitems" +msgstr "" + +#: sat/plugins/plugin_import.py:327 +#, fuzzy +msgid "An {handler_name} importer with the name {name} already exist" +msgstr "Ce nom de profile existe déjà" + +#: sat/plugins/plugin_merge_req_mercurial.py:37 +msgid "Merge request handler for Mercurial" +msgstr "" + +#: sat/plugins/plugin_merge_req_mercurial.py:40 +msgid "handle Mercurial repository" +msgstr "" + +#: sat/plugins/plugin_merge_req_mercurial.py:71 +msgid "Mercurial merge request handler initialization" +msgstr "" + +#: sat/plugins/plugin_merge_req_mercurial.py:75 +msgid "Mercurial executable (hg) not found, can't use Mercurial handler" +msgstr "" + +#: sat/plugins/plugin_merge_req_mercurial.py:116 +msgid "invalid changeset signature" +msgstr "" + +#: sat/plugins/plugin_merge_req_mercurial.py:136 +msgid "unexpected time data: {data}" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:50 +msgid "Libervia account creation" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:75 +msgid "" +"Welcome to Libervia, the web interface of Salut à Toi.\n" +"\n" +"Your account on {domain} has been successfully created.\n" +"This is a demonstration version to show you the current status of the " +"project.\n" +"It is still under development, please keep it in mind!\n" +"\n" +"Here is your connection information:\n" +"\n" +"Login on {domain}: {profile}\n" +"Jabber ID (JID): {jid}\n" +"Your password has been chosen by yourself during registration.\n" +"\n" +"In the beginning, you have nobody to talk to. To find some contacts, you " +"may use the users' directory:\n" +" - make yourself visible in \"Service / Directory subscription\".\n" +" - search for people with \"Contacts\" / Search directory\".\n" +"\n" +"Any feedback welcome. Thank you!\n" +"\n" +"Salut à Toi association\n" +"https://www.salut-a-toi.org\n" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:109 +#, fuzzy +msgid "Plugin Account initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_account.py:294 +msgid "Failed to send account creation confirmation to {email}: {msg}" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:313 +msgid "New Libervia account created" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:339 +msgid "Your Libervia account has been created" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:363 +msgid "xmpp_domain needs to be set in sat.conf. Using \"{default}\" meanwhile" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:379 +msgid "Manage your account" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:385 +#, fuzzy +msgid "Change your password" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat/plugins/plugin_misc_account.py:387 +msgid "Current profile password" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:389 +#, fuzzy +msgid "New password" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat/plugins/plugin_misc_account.py:391 +#, fuzzy +msgid "New password (again)" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat/plugins/plugin_misc_account.py:431 sat/stdui/ui_profile_manager.py:73 +#, fuzzy +msgid "The provided profile password doesn't match." +msgstr "Le fichier [%s] n'existe pas !" + +#: sat/plugins/plugin_misc_account.py:432 +msgid "Attempt failure" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:477 +msgid "The values entered for the new password are not equal." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:495 +#, fuzzy +msgid "Change your password?" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat/plugins/plugin_misc_account.py:500 +msgid "" +"Note for advanced users: this will actually change both your SàT profile " +"password AND your XMPP account password." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:504 +msgid "Continue with changing the password?" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:528 +#: sat/plugins/plugin_misc_register_account.py:133 +#, fuzzy +msgid "Confirmation" +msgstr "Connexion..." + +#: sat/plugins/plugin_misc_account.py:529 +msgid "Your password has been changed." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:533 +#: sat/plugins/plugin_misc_account.py:606 +#: sat/plugins/plugin_misc_account.py:716 sat_frontends/primitivus/base.py:790 +#: sat_frontends/primitivus/base.py:831 +#: sat_frontends/quick_frontend/quick_profile_manager.py:133 +msgid "Error" +msgstr "Erreur" + +#: sat/plugins/plugin_misc_account.py:535 +#, python-format +msgid "Your password could not be changed: %s" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:548 +#, fuzzy +msgid "Delete your account?" +msgstr "Enregistrement d'un nouveau compte" + +#: sat/plugins/plugin_misc_account.py:551 +msgid "" +"If you confirm this dialog, you will be disconnected and then your XMPP " +"account AND your SàT profile will both be DELETED." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:555 +msgid "contact list, messages history, blog posts and commentsGROUPBLOG" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:558 +msgid "contact list and messages history" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:561 +#, python-format +msgid "" +"All your data stored on %(server)s, including your %(target)s will be " +"erased." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:567 +#: sat/plugins/plugin_misc_account.py:642 +#: sat/plugins/plugin_misc_account.py:658 +#: sat/plugins/plugin_misc_account.py:674 +msgid "" +"There is no other confirmation dialog, this is the very last one! Are you" +" sure?" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:608 +#, python-format +msgid "Your XMPP account could not be deleted: %s" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:628 +msgid "Delete all your (micro-)blog posts and comments?" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:632 +msgid "" +"If you confirm this dialog, all the (micro-)blog data you submitted will " +"be erased." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:637 +msgid "These are the public and private posts and comments you sent to any group." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:649 +msgid "Delete all your (micro-)blog posts?" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:653 +msgid "" +"If you confirm this dialog, all the public and private posts you sent to " +"any group will be erased." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:665 +msgid "Delete all your (micro-)blog comments?" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:669 +msgid "" +"If you confirm this dialog, all the public and private comments you made " +"on other people's posts will be erased." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:689 +msgid "blog posts and comments" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:694 +msgid "blog posts" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:699 +msgid "comments" +msgstr "" + +#: sat/plugins/plugin_misc_account.py:705 +#, fuzzy +msgid "Deletion confirmation" +msgstr "désinscription confirmée pour [%s]" + +#: sat/plugins/plugin_misc_account.py:707 +#, python-format +msgid "Your %(target)s have been deleted." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:709 +msgid "" +"Known issue of the demo version: you need to refresh the page to make the" +" deleted posts actually disappear." +msgstr "" + +#: sat/plugins/plugin_misc_account.py:718 +#, python-format +msgid "Your %(target)s could not be deleted: %(message)s" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:50 +msgid "Manage Android platform specificities, like pause or notifications" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:92 +msgid "sound on notifications" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:94 +#, fuzzy +msgid "Normal" +msgstr "Général" + +#: sat/plugins/plugin_misc_android.py:95 sat/plugins/plugin_misc_android.py:103 +msgid "Never" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:99 +msgid "Vibrate on notifications" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:101 +#, fuzzy +msgid "Always" +msgstr "Chercher les transports" + +#: sat/plugins/plugin_misc_android.py:102 +msgid "In vibrate mode" +msgstr "" + +#: sat/plugins/plugin_misc_android.py:243 +#, fuzzy +msgid "plugin Android initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_android.py:362 +#, fuzzy +msgid "new message from {contact}" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat/plugins/plugin_misc_app_manager.py:64 +msgid "" +"Applications Manager\n" +"\n" +"Manage external applications using packagers, OS " +"virtualization/containers or other\n" +"software management tools.\n" +msgstr "" + +#: sat/plugins/plugin_misc_app_manager.py:80 +#, fuzzy +msgid "plugin Applications Manager initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_app_manager.py:166 +msgid "" +"No value found for \"public_url\", using \"example.org\" for now, please " +"set the proper value in libervia.conf" +msgstr "" + +#: sat/plugins/plugin_misc_app_manager.py:170 +msgid "" +"invalid value for \"public_url\" ({value}), it musts not start with " +"schema (\"http\"), ignoring it and using \"example.org\" instead" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:43 +msgid "Attachments handler" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:53 +#, fuzzy +msgid "plugin Attach initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_attach.py:109 +msgid "Can't resize attachment of unknown type: {attachment}" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:125 +msgid "Attachment {path!r} has been resized at {new_path!r}" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:129 +msgid "Can't resize attachment of type {main_type!r}: {attachment}" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:143 +msgid "No plugin can handle attachment with {destinee}" +msgstr "" + +#: sat/plugins/plugin_misc_attach.py:210 +msgid "certificate check disabled for upload, this is dangerous!" +msgstr "" + +#: sat/plugins/plugin_misc_debug.py:35 +msgid "Set of method to make development and debugging easier" +msgstr "" + +#: sat/plugins/plugin_misc_debug.py:41 +#, fuzzy +msgid "Plugin Debug initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_download.py:43 +msgid "File download management" +msgstr "" + +#: sat/plugins/plugin_misc_download.py:50 +#, fuzzy +msgid "plugin Download initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_download.py:95 +msgid "Can't download file: {reason}" +msgstr "" + +#: sat/plugins/plugin_misc_download.py:99 sat_frontends/jp/cmd_file.py:498 +#, fuzzy +msgid "Can't download file" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat/plugins/plugin_misc_download.py:176 +msgid "certificate check disabled for download, this is dangerous!" +msgstr "" + +#: sat/plugins/plugin_misc_download.py:187 +msgid "Can't download URI {uri}: {reason}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:44 +msgid "invitation of people without XMPP account" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:59 +msgid "You have been invited by {host_name} to {app_name}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:60 +msgid "" +"Hello {name}!\n" +"\n" +"You have received an invitation from {host_name} to participate to " +"\"{app_name}\".\n" +"To join, you just have to click on the following URL:\n" +"{url}\n" +"\n" +"Please note that this URL should not be shared with anybody!\n" +"If you want more details on {app_name}, you can check {app_url}.\n" +"\n" +"Welcome!\n" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:76 +#, fuzzy +msgid "plugin Invitations initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_email_invitation.py:105 +msgid "You can't use following key(s) in extra, they are reserved: {}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:198 +msgid "You can't use following key(s) in both args and extra: {}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:207 +msgid "You need to provide a main email address before using emails_extra" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:213 +msgid "You need to provide url_template if you use default message body" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:216 +#, fuzzy +msgid "creating an invitation" +msgstr "Connexion..." + +#: sat/plugins/plugin_misc_email_invitation.py:237 +msgid "You need to specify xmpp_domain in sat.conf" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:251 +msgid "Can't create XMPP account" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:254 +msgid "requested jid already exists, trying with {}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:265 +msgid "account {jid_} created" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:317 +msgid "somebody" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:345 +msgid "Not all arguments have been consumed: {}" +msgstr "" + +#: sat/plugins/plugin_misc_email_invitation.py:443 +msgid "Skipping reserved key {key}" +msgstr "" + +#: sat/plugins/plugin_misc_extra_pep.py:38 +msgid "Display messages from extra PEP services" +msgstr "" + +#: sat/plugins/plugin_misc_extra_pep.py:69 +#, fuzzy +msgid "Plugin Extra PEP initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_file.py:45 +msgid "" +"File Tansfer Management:\n" +"This plugin manage the various ways of sending a file, and choose the " +"best one." +msgstr "" + +#: sat/plugins/plugin_misc_file.py:52 +msgid "Please select a file to send to {peer}" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:53 +msgid "File sending" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:54 +msgid "" +"{peer} wants to send the file \"{name}\" to you:\n" +"{desc}\n" +"\n" +"The file has a size of {size_human}\n" +"\n" +"Do you accept ?" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:58 +#, fuzzy +msgid "Confirm file transfer" +msgstr "Transfert de fichier" + +#: sat/plugins/plugin_misc_file.py:59 +msgid "File {} already exists, are you sure you want to overwrite ?" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:60 +#, fuzzy +msgid "File exists" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_misc_file.py:70 +#, fuzzy +msgid "plugin File initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_file.py:82 +#, fuzzy +msgid "Action" +msgstr "Connexion..." + +#: sat/plugins/plugin_misc_file.py:82 +#, fuzzy +msgid "send file" +msgstr "Envoi un fichier" + +#: sat/plugins/plugin_misc_file.py:85 +#, fuzzy +msgid "Send a file" +msgstr "Envoi un fichier" + +#: sat/plugins/plugin_misc_file.py:121 +msgid "{name} method will be used to send the file" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:132 +msgid "Can't send {filepath} to {peer_jid} with {method_name}: {reason}" +msgstr "" + +#: sat/plugins/plugin_misc_file.py:166 sat/plugins/plugin_xep_0100.py:101 +msgid "Invalid JID" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:36 +#, fuzzy +msgid "forums management" +msgstr "Initialisation du gestionnaire de mémoire" + +#: sat/plugins/plugin_misc_forums.py:43 +msgid "forums management plugin" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:54 +#, fuzzy +msgid "forums plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_forums.py:97 +msgid "forums arguments must be a list of forums" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:109 +msgid "A forum item must be a dictionary" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:114 +msgid "following forum name is not unique: {name}" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:116 +msgid "creating missing forum node" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:130 +msgid "Unknown forum attribute: {key}" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:136 +msgid "forum need a title or a name" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:138 +msgid "forum need uri or sub-forums" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:154 +msgid "missing element" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:160 +msgid "Unexpected element: {xml}" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:168 +msgid "Following attributes are unknown: {unknown}" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:176 +msgid "invalid forum, ignoring: {xml}" +msgstr "" + +#: sat/plugins/plugin_misc_forums.py:180 +msgid "unkown forums sub element: {xml}" +msgstr "" + +#: sat/plugins/plugin_misc_groupblog.py:53 +#, fuzzy +msgid "Implementation of microblogging fine permissions" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_misc_groupblog.py:61 +#, fuzzy +msgid "Group blog plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_misc_groupblog.py:80 +msgid "Server is not able to manage item-access pubsub, we can't use group blog" +msgstr "" + +#: sat/plugins/plugin_misc_groupblog.py:86 +msgid "Server can manage group blogs" +msgstr "" + +#: sat/plugins/plugin_misc_identity.py:49 +msgid "Identity manager" +msgstr "" + +#: sat/plugins/plugin_misc_identity.py:58 +#, fuzzy +msgid "Plugin Identity initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_identity.py:293 +#: sat/plugins/plugin_misc_identity.py:365 +msgid "No callback registered for {metadata_name}" +msgstr "" + +#: sat/plugins/plugin_misc_identity.py:316 +msgid "Error while trying to get {metadata_name} with {callback}: {e}" +msgstr "" + +#: sat/plugins/plugin_misc_identity.py:376 +msgid "Error while trying to set {metadata_name} with {callback}: {e}" +msgstr "" + +#: sat/plugins/plugin_misc_identity.py:691 +msgid "Can't set metadata {metadata_name!r}: {reason}" +msgstr "" + +#: sat/plugins/plugin_misc_ip.py:57 +msgid "This plugin help to discover our external IP address." +msgstr "" + +#: sat/plugins/plugin_misc_ip.py:64 +msgid "Allow external get IP" +msgstr "" + +#: sat/plugins/plugin_misc_ip.py:67 +msgid "Confirm external site request" +msgstr "" + +#: sat/plugins/plugin_misc_ip.py:68 +msgid "" +"To facilitate data transfer, we need to contact a website.\n" +"A request will be done on {page}\n" +"That means that administrators of {domain} can know that you use " +"\"{app_name}\" and your IP Address.\n" +"\n" +"IP address is an identifier to locate you on Internet (similar to a phone" +" number).\n" +"\n" +"Do you agree to do this request ?\n" +msgstr "" + +#: sat/plugins/plugin_misc_ip.py:100 +#, fuzzy +msgid "plugin IP discovery initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_lists.py:39 +msgid "Pubsub Lists" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:47 +msgid "Pubsub lists management plugin" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:52 +msgid "TODO List" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:63 sat/plugins/plugin_misc_lists.py:113 +#: sat/plugins/plugin_misc_lists.py:156 +msgid "status" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:67 +msgid "to do" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:71 +#, fuzzy +msgid "in progress" +msgstr "Progression: " + +#: sat/plugins/plugin_misc_lists.py:75 +#, fuzzy +msgid "done" +msgstr "En ligne" + +#: sat/plugins/plugin_misc_lists.py:83 sat/plugins/plugin_misc_lists.py:180 +msgid "priority" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:87 sat/plugins/plugin_misc_lists.py:184 +msgid "major" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:91 sat/plugins/plugin_misc_lists.py:188 +#, fuzzy +msgid "normal" +msgstr "Général" + +#: sat/plugins/plugin_misc_lists.py:95 sat/plugins/plugin_misc_lists.py:192 +msgid "minor" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:106 +msgid "Grocery List" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:109 sat_frontends/jp/cmd_info.py:69 +#: sat_frontends/jp/cmd_info.py:111 +#, fuzzy +msgid "name" +msgstr "Jeu" + +#: sat/plugins/plugin_misc_lists.py:110 +msgid "quantity" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:117 +msgid "to buy" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:121 +#, fuzzy +msgid "bought" +msgstr "À propos" + +#: sat/plugins/plugin_misc_lists.py:130 +msgid "Tickets" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:140 sat_frontends/jp/cmd_info.py:69 +msgid "type" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:144 +msgid "bug" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:148 +#, fuzzy +msgid "feature request" +msgstr "Gestion des paramètres" + +#: sat/plugins/plugin_misc_lists.py:160 +#, fuzzy +msgid "queued" +msgstr "refusé" + +#: sat/plugins/plugin_misc_lists.py:164 +msgid "started" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:168 +msgid "review" +msgstr "" + +#: sat/plugins/plugin_misc_lists.py:172 +#, fuzzy +msgid "closed" +msgstr "fermeture" + +#: sat/plugins/plugin_misc_lists.py:208 +#, fuzzy +msgid "Pubsub lists plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_merge_requests.py:35 +msgid "Merge requests management" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:42 +msgid "Merge requests management plugin" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:69 +#, fuzzy +msgid "Merge requests plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_misc_merge_requests.py:121 +#, fuzzy +msgid "a handler with name {name} already exists!" +msgstr "Ce nom de profile existe déjà" + +#: sat/plugins/plugin_misc_merge_requests.py:134 +msgid "" +"merge requests of type {type} are already handled by {old_handler}, " +"ignoring {new_handler}" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:241 +msgid "repository must be specified" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:244 +msgid "{field} is set by backend, you must not set it in frontend" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:253 +msgid "{name} handler will be used" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:256 +msgid "repository {path} can't be handled by any installed handler" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:259 +msgid "no handler for this repository has been found" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:265 +msgid "No handler of this name found" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:269 +msgid "export data is empty, do you have any change to send?" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:312 +msgid "No handler can handle data type \"{type}\"" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:348 +msgid "No handler found to import {data_type}" +msgstr "" + +#: sat/plugins/plugin_misc_merge_requests.py:350 +msgid "Importing patch [{item_id}] using {name} handler" +msgstr "" + +#: sat/plugins/plugin_misc_nat_port.py:45 +msgid "Automatic NAT port mapping using UPnP" +msgstr "" + +#: sat/plugins/plugin_misc_nat_port.py:62 +#, fuzzy +msgid "plugin NAT Port initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_nat_port.py:177 +msgid "addportmapping error: {msg}" +msgstr "" + +#: sat/plugins/plugin_misc_nat_port.py:215 +msgid "error while trying to map ports: {msg}" +msgstr "" + +#: sat/plugins/plugin_misc_quiz.py:42 +#, fuzzy +msgid "Implementation of Quiz game" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_misc_quiz.py:55 +#, fuzzy +msgid "Plugin Quiz initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_quiz.py:345 +msgid "" +"Bienvenue dans cette partie rapide de quizz, le premier à atteindre le " +"score de 9 remporte le jeu\n" +"\n" +"Attention, tu es prêt ?" +msgstr "" + +#: sat/plugins/plugin_misc_quiz.py:380 sat/plugins/plugin_misc_tarot.py:664 +#, 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]" + +#: sat/plugins/plugin_misc_quiz.py:456 sat/plugins/plugin_misc_radiocol.py:353 +#, fuzzy, python-format +msgid "Unmanaged game element: %s" +msgstr "élément de jeu de carte inconnu: %s" + +#: sat/plugins/plugin_misc_radiocol.py:57 +#, fuzzy +msgid "Implementation of radio collective" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_misc_radiocol.py:76 +#, fuzzy +msgid "Radio collective initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_radiocol.py:180 +msgid "" +"The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are " +"accepted." +msgstr "" + +#: sat/plugins/plugin_misc_radiocol.py:210 +msgid "No more participants in the radiocol: cleaning data" +msgstr "" + +#: sat/plugins/plugin_misc_radiocol.py:249 +msgid "INTERNAL ERROR: can't find full path of the song to delete" +msgstr "" + +#: sat/plugins/plugin_misc_radiocol.py:258 +#, python-format +msgid "INTERNAL ERROR: can't find %s on the file system" +msgstr "" + +#: sat/plugins/plugin_misc_register_account.py:41 +#, fuzzy +msgid "Register XMPP account" +msgstr "Enregistrement d'un nouveau compte" + +#: sat/plugins/plugin_misc_register_account.py:49 +#, fuzzy +msgid "Plugin Register Account initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_register_account.py:76 +msgid "Missing values" +msgstr "" + +#: sat/plugins/plugin_misc_register_account.py:78 +#, fuzzy +msgid "No user JID or password 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/plugins/plugin_misc_register_account.py:87 +msgid "Register new account" +msgstr "Enregistrement d'un nouveau compte" + +#: sat/plugins/plugin_misc_register_account.py:92 +msgid "Do you want to register a new XMPP account {jid}?" +msgstr "" + +#: sat/plugins/plugin_misc_register_account.py:134 +#, fuzzy +msgid "Registration successful." +msgstr "Inscription réussie" + +#: sat/plugins/plugin_misc_register_account.py:138 +msgid "Failure" +msgstr "" + +#: sat/plugins/plugin_misc_register_account.py:139 +#, fuzzy, python-format +msgid "Registration failed: %s" +msgstr "Échec de l'inscription: %s" + +#: sat/plugins/plugin_misc_register_account.py:143 +#, fuzzy +msgid "Username already exists, please choose an other one." +msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" + +#: sat/plugins/plugin_misc_room_game.py:49 +msgid "Base class for MUC games" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:221 +#, python-format +msgid "%(user)s not allowed to join the game %(game)s in %(room)s" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:380 +#, python-format +msgid "%(user)s not allowed to invite for the game %(game)s in %(room)s" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:433 +#, python-format +msgid "Still waiting for %(users)s before starting the game %(game)s in %(room)s" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:472 +#, python-format +msgid "Preparing room for %s game" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:475 +#, fuzzy +msgid "Unknown profile" +msgstr "Afficher profile" + +#: sat/plugins/plugin_misc_room_game.py:583 +#, fuzzy, python-format +msgid "%(game)s game already created in room %(room)s" +msgstr "%(profile)s est déjà dans le salon %(room_jid)s" + +#: sat/plugins/plugin_misc_room_game.py:589 +#, python-format +msgid "%(game)s game in room %(room)s can only be created by %(user)s" +msgstr "" + +#: sat/plugins/plugin_misc_room_game.py:610 +#, fuzzy, python-format +msgid "Creating %(game)s game in room %(room)s" +msgstr "Construction du jeu de Tarot" + +#: sat/plugins/plugin_misc_room_game.py:615 +#: sat/plugins/plugin_misc_room_game.py:646 +#: sat/plugins/plugin_misc_tarot.py:581 +#, python-format +msgid "profile %s is unknown" +msgstr "le profil %s est inconnu" + +#: sat/plugins/plugin_misc_room_game.py:661 +#, python-format +msgid "new round for %s game" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:44 +msgid "Plugin for static blogs" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:66 +#, fuzzy +msgid "Page title" +msgstr "Petite" + +#: sat/plugins/plugin_misc_static_blog.py:68 +msgid "Banner URL" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:70 +msgid "Background image URL" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:72 +msgid "Keywords" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:74 +msgid "Description" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:97 sat/plugins/plugin_sec_otr.py:508 +#: sat/plugins/plugin_sec_otr.py:542 sat/plugins/plugin_sec_otr.py:568 +#: sat/plugins/plugin_sec_otr.py:592 +msgid "jid key is not present !" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:102 +msgid "Not available" +msgstr "" + +#: sat/plugins/plugin_misc_static_blog.py:104 +msgid "Retrieving a blog from an external domain is not implemented yet." +msgstr "" + +#: sat/plugins/plugin_misc_tarot.py:47 +#, fuzzy +msgid "Implementation of Tarot card game" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_misc_tarot.py:60 +#, fuzzy +msgid "Plugin Tarot initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_tarot.py:78 +msgid "Passe" +msgstr "Passe" + +#: sat/plugins/plugin_misc_tarot.py:79 +msgid "Petite" +msgstr "Petite" + +#: sat/plugins/plugin_misc_tarot.py:80 +msgid "Garde" +msgstr "Garde" + +#: sat/plugins/plugin_misc_tarot.py:81 +msgid "Garde Sans" +msgstr "Garde Sans" + +#: sat/plugins/plugin_misc_tarot.py:82 +msgid "Garde Contre" +msgstr "Garde Contre" + +#: sat/plugins/plugin_misc_tarot.py:171 +msgid "contrat selection" +msgstr "Sélection du contrat" + +#: sat/plugins/plugin_misc_tarot.py:189 +msgid "scores" +msgstr "points" + +#: sat/plugins/plugin_misc_tarot.py:273 sat/plugins/plugin_misc_tarot.py:313 +#, 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" + +#: sat/plugins/plugin_misc_tarot.py:327 +#, 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" + +#: sat/plugins/plugin_misc_tarot.py:338 +#: sat_frontends/primitivus/game_tarot.py:309 +msgid "Draw game" +msgstr "" + +#: sat/plugins/plugin_misc_tarot.py:341 sat/plugins/plugin_misc_tarot.py:436 +#, 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" + +#: sat/plugins/plugin_misc_tarot.py:397 +#, fuzzy +msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" +msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" + +#: sat/plugins/plugin_misc_tarot.py:422 +#, fuzzy, 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): " +"(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" + +#: sat/plugins/plugin_misc_tarot.py:507 +msgid "Internal error: unmanaged game stage" +msgstr "ERREUR INTERNE: état de jeu inconnu" + +#: sat/plugins/plugin_misc_tarot.py:530 sat/plugins/plugin_misc_tarot.py:562 +msgid "session id doesn't exist, session has probably expired" +msgstr "" + +#: sat/plugins/plugin_misc_tarot.py:540 +#, python-format +msgid "contrat [%(contrat)s] choosed by %(profile)s" +msgstr "contrat [%(contrat)s] choisi par %(profile)s" + +#: sat/plugins/plugin_misc_tarot.py:584 +#, fuzzy, python-format +msgid "Cards played by %(profile)s: [%(cards)s]" +msgstr "Cartes jouées par %(profile)s: [%(cards)s]" + +#: sat/plugins/plugin_misc_tarot.py:709 +msgid "Everybody is passing, round ended" +msgstr "" + +#: sat/plugins/plugin_misc_tarot.py:723 +#, python-format +msgid "%(player)s win the bid with %(contrat)s" +msgstr "%(player)s remporte l'enchère avec %(contrat)s" + +#: sat/plugins/plugin_misc_tarot.py:751 +#, fuzzy +msgid "tarot: chien received" +msgstr "tarot: chien reçu" + +#: sat/plugins/plugin_misc_tarot.py:828 +#, python-format +msgid "The winner of this trick is %s" +msgstr "le vainqueur de cette main est %s" + +#: sat/plugins/plugin_misc_tarot.py:896 +#, fuzzy, python-format +msgid "Unmanaged error type: %s" +msgstr "type d'erreur inconnu: %s" + +#: sat/plugins/plugin_misc_tarot.py:898 +#, python-format +msgid "Unmanaged card game element: %s" +msgstr "élément de jeu de carte inconnu: %s" + +#: sat/plugins/plugin_misc_text_commands.py:40 +msgid "IRC like text commands" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:60 +msgid "" +"Type '/help' to get a list of the available commands. If you didn't want " +"to use a command, please start your message with '//' to escape the " +"slash." +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:66 +#, fuzzy +msgid "Text commands initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_misc_text_commands.py:162 +#, python-format +msgid "Skipping not callable [%s] attribute" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:166 +msgid "Skipping cmd_ method" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:173 +msgid "Conflict for command [{old_name}], renaming it to [{new_name}]" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:180 +#, python-format +msgid "Registered text command [%s]" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:244 +#, fuzzy, python-format +msgid "Invalid command /%s. " +msgstr "Mauvais nom de profile" + +#: sat/plugins/plugin_misc_text_commands.py:277 +#, fuzzy, python-format +msgid "Unknown command /%s. " +msgstr "Type d'action inconnu" + +#: sat/plugins/plugin_misc_text_commands.py:286 +msgid "group discussions" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:288 +msgid "one to one discussions" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:290 +msgid "/{command} command only applies in {context}." +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:374 +msgid "Invalid jid, can't whois" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:380 +#, python-format +msgid "whois for %(jid)s" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:436 +msgid "Invalid command name [{}]\n" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:457 +#, python-format +msgid "" +"Text commands available:\n" +"%s" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:462 +msgid "" +"/{name}: {short_help}\n" +"{syntax}{args_help}" +msgstr "" + +#: sat/plugins/plugin_misc_text_commands.py:465 +msgid " syntax: {}\n" +msgstr "" + +#: sat/plugins/plugin_misc_text_syntaxes.py:43 sat/test/constants.py:56 +#, fuzzy +msgid "Composition" +msgstr "Connexion..." + +#: sat/plugins/plugin_misc_text_syntaxes.py:142 +msgid "Management of various text syntaxes (XHTML-IM, Markdown, etc)" +msgstr "" + +#: sat/plugins/plugin_misc_text_syntaxes.py:184 +#, fuzzy +msgid "Text syntaxes plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_misc_upload.py:41 +msgid "File upload management" +msgstr "" + +#: sat/plugins/plugin_misc_upload.py:45 +#, fuzzy +msgid "Please select a file to upload" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat/plugins/plugin_misc_upload.py:46 +msgid "File upload" +msgstr "" + +#: sat/plugins/plugin_misc_upload.py:53 +#, fuzzy +msgid "plugin Upload initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_upload.py:92 +msgid "Can't upload file: {reason}" +msgstr "" + +#: sat/plugins/plugin_misc_upload.py:96 sat_frontends/jp/cmd_file.py:586 +msgid "Can't upload file" +msgstr "" + +#: sat/plugins/plugin_misc_uri_finder.py:32 +msgid "URI finder" +msgstr "" + +#: sat/plugins/plugin_misc_uri_finder.py:39 +msgid "" +" Plugin to find URIs in well know location.\n" +" This allows to retrieve settings to work with a project (e.g. pubsub " +"node used for merge-requests).\n" +" " +msgstr "" + +#: sat/plugins/plugin_misc_uri_finder.py:52 +#, fuzzy +msgid "URI finder plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_uri_finder.py:87 +msgid "Ignoring already found uri for key \"{key}\"" +msgstr "" + +#: sat/plugins/plugin_misc_watched.py:37 +msgid "Watch for entities presence, and send notification accordingly" +msgstr "" + +#: sat/plugins/plugin_misc_watched.py:45 +#, fuzzy, python-format +msgid "Watched entity {entity} is connected" +msgstr "Vous êtes déjà connecté !" + +#: sat/plugins/plugin_misc_watched.py:62 +#, fuzzy +msgid "Watched initialisation" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_misc_welcome.py:34 +msgid "Plugin which manage welcome message and things to to on first connection." +msgstr "" + +#: sat/plugins/plugin_misc_welcome.py:42 +msgid "Display welcome message" +msgstr "" + +#: sat/plugins/plugin_misc_welcome.py:43 +msgid "Welcome to Libervia/Salut à Toi" +msgstr "" + +#: sat/plugins/plugin_misc_welcome.py:46 +msgid "" +"Welcome to a free (as in freedom) network!\n" +"\n" +"If you have any trouble, or you want to help us for the bug hunting, you " +"can contact us in real time chat by using the “Help / Official chat room”" +" menu.\n" +"\n" +"To use Libervia, you'll need to add contacts, either people you know, or " +"people you discover by using the “Contacts / Search directory” menu.\n" +"\n" +"We hope that you'll enjoy using this project.\n" +"\n" +"The Libervia/Salut à Toi Team\n" +msgstr "" + +#: sat/plugins/plugin_misc_welcome.py:75 +#, fuzzy +msgid "plugin Welcome initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_xmllog.py:36 +msgid "Send raw XML logs to bridge" +msgstr "" + +#: sat/plugins/plugin_misc_xmllog.py:51 +#, fuzzy +msgid "Activate XML log" +msgstr "Lancement du flux" + +#: sat/plugins/plugin_misc_xmllog.py:55 +#, fuzzy +msgid "Plugin XML Log initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_misc_xmllog.py:69 +msgid "XML log activated" +msgstr "" + +#: sat/plugins/plugin_misc_xmllog.py:81 +#, fuzzy +msgid "INTERNAL ERROR: Unmanaged XML type" +msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" + +#: sat/plugins/plugin_sec_aesgcm.py:48 +msgid "" +" Implementation of AES-GCM scheme, a way to encrypt files (not " +"official XMPP standard).\n" +" See https://xmpp.org/extensions/inbox/omemo-media-sharing.html for " +"details\n" +" " +msgstr "" + +#: sat/plugins/plugin_sec_aesgcm.py:63 +#, fuzzy +msgid "AESGCM plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_sec_otr.py:50 +#, fuzzy +msgid "Implementation of OTR" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_sec_otr.py:55 +msgid "OTR" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:56 +msgid "" +"To authenticate your correspondent, you need to give your below " +"fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check " +"that the one he gives you is the same as below. If there is a mismatch, " +"there can be a spy between you!" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:61 +msgid "" +"You private key is used to encrypt messages for your correspondent, " +"nobody except you must know it, if you are in doubt, you should drop it!" +"\n" +"\n" +"Are you sure you want to drop your private key?" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:67 +msgid "Some of advanced features are disabled !" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:169 +#, python-format +msgid "/!\\ conversation with %(other_jid)s is now UNENCRYPTED" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:182 +#, fuzzy +msgid "trusted" +msgstr "refusé" + +#: sat/plugins/plugin_sec_otr.py:182 +#, fuzzy +msgid "untrusted" +msgstr "refusé" + +#: sat/plugins/plugin_sec_otr.py:185 +msgid "{trusted} OTR conversation with {other_jid} REFRESHED" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:189 +msgid "" +"{trusted} encrypted OTR conversation started with {other_jid}\n" +"{extra_info}" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:201 +msgid "OTR conversation with {other_jid} is FINISHED" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:209 +msgid "Unknown OTR state" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:249 +msgid "Save is called but privkey is None !" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:308 +#, fuzzy +msgid "OTR plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_sec_otr.py:418 +msgid "You have no private key yet, start an OTR conversation to have one" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:424 +msgid "No private key" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:436 +msgid "" +"Your fingerprint is:\n" +"{fingerprint}\n" +"\n" +"Start an OTR conversation to have your correspondent one." +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:442 sat/plugins/plugin_xep_0384.py:687 +msgid "Fingerprint" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:453 +msgid "Your correspondent {correspondent} is now TRUSTED" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:459 +msgid "Your correspondent {correspondent} is now UNTRUSTED" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:477 +msgid "Authentication ({entity_jid})" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:483 +msgid "" +"Your own fingerprint is:\n" +"{fingerprint}" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:486 +msgid "" +"Your correspondent fingerprint should be:\n" +"{fingerprint}" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:492 +msgid "Is your correspondent fingerprint the same as here ?" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:494 +msgid "yes" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:494 +msgid "no" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:520 +msgid "" +"Can't start an OTR session, there is already an encrypted session with " +"{name}" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:598 +msgid "You don't have a private key yet !" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:611 +msgid "Your private key has been dropped" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:620 +msgid "Confirm private key drop" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:649 +msgid "WARNING: received unencrypted data in a supposedly encrypted context" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:656 +msgid "WARNING: received OTR encrypted data in an unencrypted context" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:662 +msgid "WARNING: received OTR error message: {msg}" +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:668 +#, fuzzy, python-format +msgid "Error while trying de decrypt OTR message: {msg}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat/plugins/plugin_sec_otr.py:780 +msgid "" +"Your message was not sent because your correspondent closed the encrypted" +" conversation on his/her side. Either close your own side, or refresh the" +" session." +msgstr "" + +#: sat/plugins/plugin_sec_otr.py:785 +msgid "Message discarded because closed encryption channel" +msgstr "" + +#: sat/plugins/plugin_syntax_wiki_dotclear.py:40 +#, fuzzy +msgid "Implementation of Dotclear wiki syntax" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_syntax_wiki_dotclear.py:664 +#, fuzzy +msgid "Dotclear wiki syntax plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_tickets_import.py:38 +msgid "" +"Tickets import management:\n" +"This plugin manage the different tickets importers which can register to " +"it, and handle generic importing tasks." +msgstr "" + +#: sat/plugins/plugin_tickets_import.py:57 +#, fuzzy +msgid "plugin Tickets Import initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_tickets_import.py:111 +msgid "comments_uri key will be generated and must not be used by importer" +msgstr "" + +#: sat/plugins/plugin_tickets_import.py:115 +msgid "{key} must be a list" +msgstr "" + +#: sat/plugins/plugin_tickets_import.py:174 +msgid "mapping option must be a dictionary" +msgstr "" + +#: sat/plugins/plugin_tickets_import.py:179 +msgid "keys and values of mapping must be sources and destinations ticket fields" +msgstr "" + +#: sat/plugins/plugin_tickets_import_bugzilla.py:41 +msgid "Tickets importer for Bugzilla" +msgstr "" + +#: sat/plugins/plugin_tickets_import_bugzilla.py:44 +msgid "import tickets from Bugzilla xml export file" +msgstr "" + +#: sat/plugins/plugin_tickets_import_bugzilla.py:46 +msgid "" +"This importer handle Bugzilla xml export file.\n" +"\n" +"To use it, you'll need to export tickets using XML.\n" +"Tickets will be uploaded with the same ID as for Bugzilla, any existing " +"ticket with this ID will be replaced.\n" +"\n" +"location: you must use the absolute path to your .xml file\n" +msgstr "" + +#: sat/plugins/plugin_tickets_import_bugzilla.py:128 +#, fuzzy +msgid "Bugilla Import plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_tmp_directory_subscription.py:37 +#, fuzzy +msgid "Implementation of directory subscription" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_tmp_directory_subscription.py:47 +#, fuzzy +msgid "Directory subscription plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_tmp_directory_subscription.py:50 +#: sat/plugins/plugin_xep_0050.py:315 sat/plugins/plugin_xep_0100.py:84 +msgid "Service" +msgstr "" + +#: sat/plugins/plugin_tmp_directory_subscription.py:50 +msgid "Directory subscription" +msgstr "" + +#: sat/plugins/plugin_tmp_directory_subscription.py:53 +msgid "User directory subscription" +msgstr "" + +#: sat/plugins/plugin_xep_0020.py:46 +#, fuzzy +msgid "Implementation of Feature Negotiation" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0020.py:52 +#, fuzzy +msgid "Plugin XEP_0020 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0020.py:104 +msgid "More than one value choosed for {}, keeping the first one" +msgstr "" + +#: sat/plugins/plugin_xep_0033.py:66 +#, fuzzy +msgid "Implementation of Extended Stanza Addressing" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0033.py:76 +#, fuzzy +msgid "Extended Stanza Addressing plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0033.py:97 +msgid "XEP-0033 is being used but the server doesn't support it!" +msgstr "" + +#: sat/plugins/plugin_xep_0033.py:103 +#, fuzzy +msgid " or " +msgstr "Formulaire" + +#: sat/plugins/plugin_xep_0033.py:105 +#, python-format +msgid "" +"Stanzas using XEP-0033 should be addressed to %(expected)s, not " +"%(current)s!" +msgstr "" + +#: sat/plugins/plugin_xep_0033.py:111 +msgid "TODO: addressing has been fixed by the backend... fix it in the frontend!" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:56 +#, fuzzy +msgid "Implementation of Multi-User Chat" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0045.py:89 +#, fuzzy +msgid "Plugin XEP_0045 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0045.py:145 +msgid "MUC" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:145 +#, fuzzy +msgid "configure" +msgstr " Configurer l'application" + +#: sat/plugins/plugin_xep_0045.py:146 +#, fuzzy +msgid "Configure Multi-User Chat room" +msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: sat/plugins/plugin_xep_0045.py:194 +msgid "" +"Received non delayed message in a room before its initialisation: " +"state={state}, msg={msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:216 sat/plugins/plugin_xep_0045.py:224 +#: sat/plugins/plugin_xep_0045.py:880 +msgid "This room has not been joined" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:283 +msgid "Room joining cancelled by user" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:288 +msgid "Rooms in {}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:303 +msgid "room locked !" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:306 +#, fuzzy +msgid "Error while configuring the room: {failure_}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat/plugins/plugin_xep_0045.py:322 +msgid "Room {} is restricted" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:323 +msgid "This room is restricted, please enter the password" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:332 +#, fuzzy, python-format +msgid "Error while joining the room {room}{suffix}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat/plugins/plugin_xep_0045.py:334 +msgid "Group chat error" +msgstr "Erreur de salon de discussion" + +#: sat/plugins/plugin_xep_0045.py:401 +msgid "room_jid key is not present !" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:406 +msgid "No configuration available for this room" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:440 sat/plugins/plugin_xep_0045.py:442 +msgid "Session ID doesn't exist, session has probably expired." +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:441 +#, fuzzy +msgid "Room configuration failed" +msgstr "confirmation de type Oui/Non demandée" + +#: sat/plugins/plugin_xep_0045.py:447 +#, fuzzy +msgid "Room configuration succeed" +msgstr "confirmation de type Oui/Non demandée" + +#: sat/plugins/plugin_xep_0045.py:448 +msgid "The new settings have been saved." +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:509 +msgid "No MUC service found on main server" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:538 +msgid "" +"Invalid room identifier: {room_id}'. Please give a room short or full " +"identifier like 'room' or 'room@{muc_service}'." +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:558 +#, fuzzy, python-format +msgid "{profile} is already in room {room_jid}" +msgstr "%(profile)s est déjà dans le salon %(room_jid)s" + +#: sat/plugins/plugin_xep_0045.py:561 +#, fuzzy, python-format +msgid "[{profile}] is joining room {room} with nick {nick}" +msgstr "[%(profile)s] rejoint %(room)s avec %(nick)s" + +#: sat/plugins/plugin_xep_0045.py:729 +msgid "You must provide a member's nick to kick." +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:738 +msgid "You have kicked {}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:740 sat/plugins/plugin_xep_0045.py:776 +msgid " for the following reason: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:763 +msgid "You must provide a valid JID to ban, like in '/ban contact@example.net'" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:774 +msgid "You have banned {}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:802 +msgid "" +"You must provide a valid JID to affiliate, like in '/affiliate " +"contact@example.net member'" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:808 +#, python-format +msgid "You must provide a valid affiliation: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:815 +msgid "New affiliation for {entity}: {affiliation}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:862 +msgid "No known default MUC service {unparsed}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:867 +#, fuzzy, python-format +msgid "{} is not a valid JID!" +msgstr "%s n'est pas un JID valide !" + +#: sat/plugins/plugin_xep_0045.py:885 +#, fuzzy, python-format +msgid "Nickname: %s" +msgstr "fichier enregistré dans %s" + +#: sat/plugins/plugin_xep_0045.py:887 +#, python-format +msgid "Entity: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:889 +#, python-format +msgid "Affiliation: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:891 +#, fuzzy, python-format +msgid "Role: %s" +msgstr "Profile:" + +#: sat/plugins/plugin_xep_0045.py:893 +#, fuzzy, python-format +msgid "Status: %s" +msgstr "Sélection du contrat" + +#: sat/plugins/plugin_xep_0045.py:895 +#, python-format +msgid "Show: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:960 +msgid "" +"room {room} is not in expected state: room is in state {current_state} " +"while we were expecting {expected_state}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1093 +msgid "No message received while offline in {room_jid}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1097 +msgid "We have received {num_mess} message(s) in {room_jid} while offline." +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1141 +msgid "missing nick in presence: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1217 +#, fuzzy, python-format +msgid "user {nick} has joined room {room_id}" +msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" + +#: sat/plugins/plugin_xep_0045.py:1234 +msgid "=> {} has joined the room" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1253 +#, fuzzy, python-format +msgid "Room ({room}) left ({profile})" +msgstr "contrat [%(contrat)s] choisi par %(profile)s" + +#: sat/plugins/plugin_xep_0045.py:1267 +#, fuzzy, python-format +msgid "user {nick} left room {room_id}" +msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" + +#: sat/plugins/plugin_xep_0045.py:1279 +msgid "<= {} has left the room" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1342 +msgid "received history in unexpected state in room {room} (state: {state})" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1350 +msgid "storing the unexpected message anyway, to avoid loss" +msgstr "" + +#: sat/plugins/plugin_xep_0045.py:1437 +#, fuzzy, python-format +msgid "New subject for room ({room_id}): {subject}" +msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" + +#: sat/plugins/plugin_xep_0047.py:62 +#, fuzzy +msgid "Implementation of In-Band Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: sat/plugins/plugin_xep_0047.py:71 +#, fuzzy +msgid "In-Band Bytestreams plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0047.py:162 +msgid "IBB stream opening" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:171 +#, python-format +msgid "Ignoring unexpected IBB transfer: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:176 +msgid "sended jid inconsistency (man in the middle attack attempt ?)" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:206 +msgid "IBB stream closing" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:228 +#, fuzzy +msgid "Received data for an unknown session id" +msgstr "Confirmation inconnue reçue" + +#: sat/plugins/plugin_xep_0047.py:236 +msgid "" +"sended jid inconsistency (man in the middle attack attempt ?)\n" +"initial={initial}\n" +"given={given}" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:246 +msgid "Sequence error" +msgstr "" + +#: sat/plugins/plugin_xep_0047.py:261 +msgid "Invalid base64 data" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:45 +#, fuzzy +msgid "Implementation of bookmarks" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0048.py:58 +#, fuzzy +msgid "Bookmarks plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0048.py:63 sat_frontends/primitivus/base.py:540 +msgid "Groups" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:63 +msgid "Bookmarks" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:66 +msgid "Use and manage bookmarks" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:147 +msgid "Private XML storage not available" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:260 +#, fuzzy +msgid "No room jid selected" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_xep_0048.py:280 +msgid "Bookmarks manager" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:310 sat_frontends/jp/cmd_bookmarks.py:126 +msgid "add a bookmark" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:312 +#, fuzzy +msgid "Name" +msgstr "Jeu" + +#: sat/plugins/plugin_xep_0048.py:314 sat_frontends/jp/cmd_profile.py:175 +msgid "jid" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:316 +msgid "Nickname" +msgstr "Surnon" + +#: sat/plugins/plugin_xep_0048.py:318 +msgid "Autojoin" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:321 sat_frontends/primitivus/xmlui.py:470 +msgid "Save" +msgstr "Sauvegarder" + +#: sat/plugins/plugin_xep_0048.py:367 +msgid "Bookmarks will be local only" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:368 +#, python-format +msgid "Type selected for \"auto\" storage: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:500 +msgid "Bad arguments" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:509 +#, python-format +msgid "All [%s] bookmarks are being removed" +msgstr "" + +#: sat/plugins/plugin_xep_0048.py:520 +msgid "Bookmark added" +msgstr "" + +#: sat/plugins/plugin_xep_0049.py:37 +#, fuzzy +msgid "Implementation of private XML storage" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0049.py:45 +#, fuzzy +msgid "Plugin XEP-0049 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0050.py:51 +#: sat_frontends/quick_frontend/constants.py:29 +msgid "Online" +msgstr "En ligne" + +#: sat/plugins/plugin_xep_0050.py:52 +msgid "Away" +msgstr "Absent" + +#: sat/plugins/plugin_xep_0050.py:53 +#: sat_frontends/quick_frontend/constants.py:30 +msgid "Free for chat" +msgstr "Libre pour discuter" + +#: sat/plugins/plugin_xep_0050.py:54 +#: sat_frontends/quick_frontend/constants.py:32 +msgid "Do not disturb" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:55 +msgid "Left" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:56 sat_frontends/primitivus/base.py:535 +#, fuzzy +msgid "Disconnect" +msgstr "Déconnexion..." + +#: sat/plugins/plugin_xep_0050.py:67 +#, fuzzy +msgid "Implementation of Ad-Hoc Commands" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0050.py:126 +#, fuzzy, python-format +msgid "The groups [{group}] is unknown for profile [{profile}])" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat/plugins/plugin_xep_0050.py:284 +#, fuzzy +msgid "plugin XEP-0050 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0050.py:315 +#, fuzzy +msgid "Commands" +msgstr "Mauvais nom de profile" + +#: sat/plugins/plugin_xep_0050.py:318 +msgid "Execute ad-hoc commands" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:329 +msgid "Status" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:364 +msgid "Missing command element" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:379 +#, fuzzy +msgid "Please select a command" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat/plugins/plugin_xep_0050.py:397 +#, fuzzy, python-format +msgid "Invalid note type [%s], using info" +msgstr "Type d'action inconnu" + +#: sat/plugins/plugin_xep_0050.py:408 +msgid "WARNING" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:409 +#, fuzzy +msgid "ERROR" +msgstr "Erreur" + +#: sat/plugins/plugin_xep_0050.py:457 +msgid "No known payload found in ad-hoc command result, aborting" +msgstr "" + +#: sat/plugins/plugin_xep_0050.py:464 +#, fuzzy +msgid "No payload found" +msgstr "Aucune donnée trouvée" + +#: sat/plugins/plugin_xep_0050.py:574 +#, fuzzy +msgid "Please enter target jid" +msgstr "Veuillez entrer le JID de votre nouveau contact" + +#: sat/plugins/plugin_xep_0050.py:588 +#, fuzzy +msgid "status selection" +msgstr "Sélection du contrat" + +#: sat/plugins/plugin_xep_0050.py:618 +msgid "Status updated" +msgstr "" + +#: sat/plugins/plugin_xep_0054.py:64 +msgid "Implementation of vcard-temp" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0054.py:84 +msgid "Plugin XEP_0054 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0054.py:99 +msgid "No avatar in cache for {profile}" +msgstr "" + +#: sat/plugins/plugin_xep_0054.py:137 +msgid "Decoding binary" +msgstr "Décodage des données" + +#: sat/plugins/plugin_xep_0054.py:242 +msgid "vCard element not found for {entity_jid}: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0054.py:287 +msgid "Can't get vCard for {entity_jid}: {e}" +msgstr "" + +#: sat/plugins/plugin_xep_0054.py:291 +msgid "VCard found" +msgstr "VCard trouvée" + +#: sat/plugins/plugin_xep_0055.py:53 +#, fuzzy +msgid "Implementation of Jabber Search" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0055.py:70 +#, fuzzy +msgid "Jabber search plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0055.py:100 sat/stdui/ui_contact_list.py:39 +#: sat/stdui/ui_contact_list.py:45 sat/stdui/ui_contact_list.py:51 +#: sat_frontends/primitivus/base.py:539 +#: sat_frontends/primitivus/contact_list.py:50 +#, fuzzy +msgid "Contacts" +msgstr "&Contacts" + +#: sat/plugins/plugin_xep_0055.py:100 +msgid "Search directory" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:103 +msgid "Search user directory" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:143 +#, fuzzy, python-format +msgid "Search users" +msgstr "Remplacement de l'utilisateur %s" + +#: sat/plugins/plugin_xep_0055.py:174 +msgid "Search for" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:181 +msgid "Simple search" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:191 sat/plugins/plugin_xep_0055.py:305 +msgid "Search" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:226 +msgid "Advanced search" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:246 +msgid "Search on" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:248 +msgid "Other service" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:256 +msgid "Refresh fields" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:260 +msgid "Displaying the search form for" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:341 +msgid "Search results" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:346 +msgid "The search gave no result" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:386 sat/plugins/plugin_xep_0055.py:493 +msgid "No query element found" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:391 sat/plugins/plugin_xep_0055.py:498 +msgid "No data form found" +msgstr "Aucune donnée trouvée" + +#: sat/plugins/plugin_xep_0055.py:403 +#, fuzzy, python-format +msgid "Fields request failure: %s" +msgstr "Échec de l'inscription: %s" + +#: sat/plugins/plugin_xep_0055.py:478 +msgid "The search could not be performed" +msgstr "" + +#: sat/plugins/plugin_xep_0055.py:510 +#, fuzzy, python-format +msgid "Search request failure: %s" +msgstr "Échec de la désinscription: %s" + +#: sat/plugins/plugin_xep_0059.py:42 +#, fuzzy +msgid "Implementation of Result Set Management" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0059.py:52 +#, fuzzy +msgid "Result Set Management plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0059.py:65 +msgid "rsm_max can't be negative" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:56 +#, fuzzy +msgid "Implementation of PubSub Protocol" +msgstr "Implémentation du protocole de transports" + +#: sat/plugins/plugin_xep_0060.py:95 +#, fuzzy +msgid "PubSub plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0060.py:323 +msgid "Can't retrieve pubsub_service from conf, we'll use first one that we find" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:487 +msgid "Can't parse items: {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:556 +msgid "Invalid item: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:572 +msgid "" +"Can't use publish-options ({options}) on node {node}, re-publishing " +"without them: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:905 sat/plugins/plugin_xep_0060.py:948 +msgid "Invalid result: missing element: {}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:916 sat/plugins/plugin_xep_0060.py:961 +msgid "Invalid result: bad element: {}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:1284 +msgid "Invalid result: missing element: {}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:1289 +msgid "Invalid result: {}" +msgstr "" + +#: sat/plugins/plugin_xep_0060.py:1299 +msgid "Invalid result: bad element: {}" +msgstr "" + +#: sat/plugins/plugin_xep_0065.py:90 +msgid "Implementation of SOCKS5 Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: sat/plugins/plugin_xep_0065.py:528 +msgid "File transfer completed, closing connection" +msgstr "Transfert de fichier terminé, fermeture de la connexion" + +#: sat/plugins/plugin_xep_0065.py:695 +#, python-format +msgid "Socks 5 client connection lost (reason: %s)" +msgstr "Connexion du client SOCKS5 perdue (raison: %s)" + +#: sat/plugins/plugin_xep_0065.py:723 +msgid "Plugin XEP_0065 initialization" +msgstr "Initialisation du plugin XEP_0065" + +#: sat/plugins/plugin_xep_0065.py:781 +#, fuzzy, python-format +msgid "Socks5 Stream server launched on port {}" +msgstr "Lancement du serveur de flux Socks5 sur le port %d" + +#: sat/plugins/plugin_xep_0070.py:56 +#, fuzzy +msgid "Implementation of HTTP Requests via XMPP" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0070.py:66 +#, fuzzy +msgid "Plugin XEP_0070 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0070.py:79 +msgid "XEP-0070 Verifying HTTP Requests via XMPP (iq)" +msgstr "" + +#: sat/plugins/plugin_xep_0070.py:88 +msgid "XEP-0070 Verifying HTTP Requests via XMPP (message)" +msgstr "" + +#: sat/plugins/plugin_xep_0070.py:98 +#, fuzzy +msgid "Auth confirmation" +msgstr "Connexion..." + +#: sat/plugins/plugin_xep_0070.py:99 +msgid "" +"{auth_url} needs to validate your identity, do you agree?\n" +"Validation code : {auth_id}\n" +"\n" +"Please check that this code is the same as on {auth_url}" +msgstr "" + +#: sat/plugins/plugin_xep_0070.py:117 +msgid "XEP-0070 reply iq" +msgstr "" + +#: sat/plugins/plugin_xep_0070.py:122 +msgid "XEP-0070 reply message" +msgstr "" + +#: sat/plugins/plugin_xep_0070.py:127 +msgid "XEP-0070 reply error" +msgstr "" + +#: sat/plugins/plugin_xep_0071.py:55 +#, fuzzy +msgid "Implementation of XHTML-IM" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0071.py:94 +#, fuzzy +msgid "XHTML-IM plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0071.py:223 +msgid "Can't have XHTML and rich content at the same time" +msgstr "" + +#: sat/plugins/plugin_xep_0077.py:41 +msgid "Implementation of in-band registration" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0077.py:54 +#, fuzzy +msgid "Registration asked for {jid}" +msgstr "Éched de l'insciption (%s)" + +#: sat/plugins/plugin_xep_0077.py:79 +msgid "Stream started with {server}, now registering" +msgstr "" + +#: sat/plugins/plugin_xep_0077.py:85 +#, fuzzy, python-format +msgid "Registration answer: {}" +msgstr "réponse à la demande d'inscription: %s" + +#: sat/plugins/plugin_xep_0077.py:89 +#, fuzzy, python-format +msgid "Registration failure: {}" +msgstr "Échec de l'inscription: %s" + +#: sat/plugins/plugin_xep_0077.py:116 +msgid "Plugin XEP_0077 initialization" +msgstr "Initialisation du plugin XEP_0077" + +#: sat/plugins/plugin_xep_0077.py:176 +#, fuzzy +msgid "Can't find data form" +msgstr "Impossible de trouver la VCard de %s" + +#: sat/plugins/plugin_xep_0077.py:178 +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é :(" + +#: sat/plugins/plugin_xep_0077.py:202 sat/plugins/plugin_xep_0077.py:212 +#, python-format +msgid "Registration failure: %s" +msgstr "Échec de l'inscription: %s" + +#: sat/plugins/plugin_xep_0077.py:206 +#, python-format +msgid "registration answer: %s" +msgstr "réponse à la demande d'inscription: %s" + +#: sat/plugins/plugin_xep_0077.py:215 +msgid "Username already exists, please choose an other one" +msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" + +#: sat/plugins/plugin_xep_0077.py:229 +#, fuzzy, python-format +msgid "Asking registration for {}" +msgstr "Demande d'enregistrement pour [%s]" + +#: sat/plugins/plugin_xep_0085.py:55 +#, fuzzy +msgid "Implementation of Chat State Notifications Protocol" +msgstr "Implémentation du protocole de transports" + +#: sat/plugins/plugin_xep_0085.py:97 +msgid "Enable chat state notifications" +msgstr "" + +#: sat/plugins/plugin_xep_0085.py:101 +#, fuzzy +msgid "Chat State Notifications plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0092.py:42 +#, fuzzy +msgid "Implementation of Software Version" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0092.py:48 +#, fuzzy +msgid "Plugin XEP_0092 initialization" +msgstr "Initialisation du plugin XEP_0096" + +#: sat/plugins/plugin_xep_0092.py:119 +#, fuzzy, python-format +msgid "Client name: %s" +msgstr "fichier enregistré dans %s" + +#: sat/plugins/plugin_xep_0092.py:121 +#, python-format +msgid "Client version: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0092.py:123 +#, fuzzy, python-format +msgid "Operating system: %s" +msgstr "réponse à la demande d'inscription: %s" + +#: sat/plugins/plugin_xep_0092.py:128 +msgid "Software version not available" +msgstr "" + +#: sat/plugins/plugin_xep_0092.py:130 +msgid "Client software version request timeout" +msgstr "" + +#: sat/plugins/plugin_xep_0095.py:41 +#, fuzzy +msgid "Implementation of Stream Initiation" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0095.py:54 +#, fuzzy +msgid "Plugin XEP_0095 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0095.py:84 +#, fuzzy +msgid "XEP-0095 Stream initiation" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0095.py:127 +msgid "sending stream initiation accept answer" +msgstr "" + +#: sat/plugins/plugin_xep_0095.py:168 +#, python-format +msgid "Stream Session ID: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0096.py:48 +msgid "Implementation of SI File Transfer" +msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: sat/plugins/plugin_xep_0096.py:55 +#, fuzzy +msgid "Stream Initiation" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0096.py:58 +msgid "Plugin XEP_0096 initialization" +msgstr "Initialisation du plugin XEP_0096" + +#: sat/plugins/plugin_xep_0096.py:129 +msgid "XEP-0096 file transfer requested" +msgstr "" + +#: sat/plugins/plugin_xep_0096.py:377 +#, fuzzy, python-format +msgid "The contact {} has refused your file" +msgstr "Le contact %s a refusé votre inscription" + +#: sat/plugins/plugin_xep_0096.py:378 +#, fuzzy +msgid "File refused" +msgstr "refusé" + +#: sat/plugins/plugin_xep_0096.py:381 +msgid "Error during file transfer" +msgstr "" + +#: sat/plugins/plugin_xep_0096.py:382 +msgid "" +"Something went wrong during the file transfer session initialisation: " +"{reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0096.py:385 +#, fuzzy +msgid "File transfer error" +msgstr "Transfert de fichier" + +#: sat/plugins/plugin_xep_0096.py:394 +#, fuzzy, python-format +msgid "transfer {sid} successfuly finished [{profile}]" +msgstr "Transfert [%s] refusé" + +#: sat/plugins/plugin_xep_0096.py:402 +msgid "transfer {sid} failed [{profile}]: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:37 +msgid "Implementation of Gateways protocol" +msgstr "Implémentation du protocole de transports" + +#: sat/plugins/plugin_xep_0100.py:40 +#, fuzzy +msgid "" +"Be careful ! Gateways allow you to use an external IM (legacy IM), so you" +" can see your contact as XMPP 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, analysed 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)." + +#: sat/plugins/plugin_xep_0100.py:48 +msgid "Internet Relay Chat" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:49 +msgid "XMPP" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:50 +msgid "Tencent QQ" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:51 +msgid "SIP/SIMPLE" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:52 +msgid "ICQ" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:53 +msgid "Yahoo! Messenger" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:54 +msgid "Gadu-Gadu" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:55 +msgid "AOL Instant Messenger" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:56 +msgid "Windows Live Messenger" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:62 +msgid "Gateways plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0100.py:84 +#, fuzzy +msgid "Gateways" +msgstr "Chercher les transports" + +#: sat/plugins/plugin_xep_0100.py:87 +#, fuzzy +msgid "Find gateways" +msgstr "Chercher les transports" + +#: sat/plugins/plugin_xep_0100.py:108 +#, fuzzy, python-format +msgid "Gateways manager (%s)" +msgstr "Gestionnaire de transport" + +#: sat/plugins/plugin_xep_0100.py:121 +#, python-format +msgid "Failed (%s)" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:134 +#, fuzzy +msgid "Use external XMPP server" +msgstr "Utiliser un autre serveur XMPP:" + +#: sat/plugins/plugin_xep_0100.py:136 +msgid "Go !" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:143 +#, fuzzy +msgid "No gateway index selected" +msgstr "Aucun profile sélectionné" + +#: sat/plugins/plugin_xep_0100.py:158 +#, python-format +msgid "" +"INTERNAL ERROR: identity category should always be \"gateway\" in " +"_getTypeString, got \"%s\"" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:166 +msgid "Unknown IM" +msgstr "Messagerie inconnue" + +#: sat/plugins/plugin_xep_0100.py:170 +msgid "Registration successful, doing the rest" +msgstr "Inscription réussie, lancement du reste de la procédure" + +#: sat/plugins/plugin_xep_0100.py:195 +msgid "Timeout" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:211 +#, fuzzy, python-format +msgid "Found gateway [%(jid)s]: %(identity_name)s" +msgstr "Transport trouvé (%(jid)s): %(identity)s" + +#: sat/plugins/plugin_xep_0100.py:222 +#, python-format +msgid "Skipping [%(jid)s] which is not a gateway" +msgstr "" + +#: sat/plugins/plugin_xep_0100.py:231 +msgid "No gateway found" +msgstr "Aucun transport trouvé" + +#: sat/plugins/plugin_xep_0100.py:236 +#, python-format +msgid "item found: %s" +msgstr "object trouvé: %s" + +#: sat/plugins/plugin_xep_0100.py:260 +#, fuzzy, python-format +msgid "find gateways (target = %(target)s, profile = %(profile)s)" +msgstr "transports trouvée (cible = %s)" + +#: sat/plugins/plugin_xep_0106.py:38 +msgid "(Un)escape JID to use disallowed chars in local parts" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:50 +#, fuzzy +msgid "Implementation of entity capabilities" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0115.py:58 +#, fuzzy +msgid "Plugin XEP_0115 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0115.py:73 +msgid "Caps optimisation enabled" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:76 +msgid "Caps optimisation not available" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:154 +#, python-format +msgid "Received invalid capabilities tag: %s" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:170 +msgid "" +"Unknown hash method for entity capabilities: [{hash_method}] (entity: " +"{entity_jid}, node: {node})" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:183 +msgid "" +"Computed hash differ from given hash:\n" +"given: [{given}]\n" +"computed: [{computed}]\n" +"(entity: {entity_jid}, node: {node})" +msgstr "" + +#: sat/plugins/plugin_xep_0115.py:205 +msgid "Couldn't retrieve disco info for {jid}: {error}" +msgstr "" + +#: sat/plugins/plugin_xep_0163.py:42 +#, fuzzy +msgid "Implementation of Personal Eventing Protocol" +msgstr "Implémentation du protocole de transports" + +#: sat/plugins/plugin_xep_0163.py:48 +#, fuzzy +msgid "PEP plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0163.py:131 +#, fuzzy, python-format +msgid "Trying to send personal event with an unknown profile key [%s]" +msgstr "Tentative d'appel d'un profile inconnue" + +#: sat/plugins/plugin_xep_0163.py:136 +#, fuzzy +msgid "Trying to send personal event for an unknown type" +msgstr "Tentative d'assigner un paramètre à un profile inconnu" + +#: sat/plugins/plugin_xep_0163.py:142 +#, fuzzy +msgid "No item found" +msgstr "Aucun transport trouvé" + +#: sat/plugins/plugin_xep_0163.py:149 +msgid "Can't find mood element in mood event" +msgstr "" + +#: sat/plugins/plugin_xep_0163.py:153 +#, fuzzy +msgid "No mood found" +msgstr "Aucune donnée trouvée" + +#: sat/plugins/plugin_xep_0166.py:50 +msgid "{entity} want to start a jingle session with you, do you accept ?" +msgstr "" + +#: sat/plugins/plugin_xep_0166.py:60 +#, fuzzy +msgid "Implementation of Jingle" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0166.py:98 +#, fuzzy +msgid "plugin Jingle initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0166.py:156 +#, fuzzy, python-format +msgid "Error while terminating session: {msg}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat/plugins/plugin_xep_0166.py:395 +msgid "You can't do a jingle session with yourself" +msgstr "" + +#: sat/plugins/plugin_xep_0166.py:511 +msgid "Confirm Jingle session" +msgstr "" + +#: sat/plugins/plugin_xep_0184.py:71 +#, fuzzy +msgid "Implementation of Message Delivery Receipts" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0184.py:96 +msgid "Enable message delivery receipts" +msgstr "" + +#: sat/plugins/plugin_xep_0184.py:100 +#, fuzzy +msgid "Plugin XEP_0184 (message delivery receipts) initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0184.py:136 +msgid "[XEP-0184] Request acknowledgment for message id {}" +msgstr "" + +#: sat/plugins/plugin_xep_0184.py:180 +msgid "[XEP-0184] Receive acknowledgment for message id {}" +msgstr "" + +#: sat/plugins/plugin_xep_0184.py:190 +msgid "[XEP-0184] Delete waiting acknowledgment for message id {}" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:46 +#, fuzzy +msgid "Implementation of Stream Management" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0198.py:134 +#, fuzzy +msgid "Plugin Stream Management initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0198.py:144 +msgid "Invalid ack_timeout value, please check your configuration" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:147 +msgid "Ack timeout disabled" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:149 +msgid "Ack timeout set to {timeout}s" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:171 +msgid "" +"Your server doesn't support stream management ({namespace}), this is used" +" to improve connection problems detection (like network outages). Please " +"ask your server administrator to enable this feature." +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:302 +msgid "" +"Connection failed using location given by server (host: {host}, port: " +"{port}), switching to normal host and port (host: {normal_host}, port: " +"{normal_port})" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:317 +msgid "Incorrect element received, no \"id\" attribute" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:319 +msgid "" +"You're server doesn't support session resuming with stream management, " +"please contact your server administrator to enable it" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:341 +msgid "Invalid location received: {location}" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:359 +msgid "Invalid \"max\" attribute" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:361 +msgid "Using default session max value ({max_s} s)." +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:363 +msgid "Stream Management enabled" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:367 +msgid "Stream Management enabled, with a resumption time of {res_m:.2f} min" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:382 +msgid "" +"Stream session resumed (disconnected for {d_time} s, {count} stanza(s) " +"resent)" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:395 +msgid "Can't use stream management" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:399 +msgid "{msg}: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:408 +msgid "stream resumption not possible, restarting full session" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:495 +msgid "Server returned invalid ack element, disabling stream management: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:501 +msgid "Server acked more stanzas than we have sent, disabling stream management." +msgstr "" + +#: sat/plugins/plugin_xep_0198.py:511 +msgid "Ack was not received in time, aborting connection" +msgstr "" + +#: sat/plugins/plugin_xep_0199.py:39 +#, fuzzy +msgid "Implementation of XMPP Ping" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0199.py:49 +#, fuzzy +msgid "XMPP Ping plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0199.py:109 +msgid "ping error ({err_msg}). Response time: {time} s" +msgstr "" + +#: sat/plugins/plugin_xep_0199.py:123 +msgid "Invalid jid: \"{entity_jid}\"" +msgstr "" + +#: sat/plugins/plugin_xep_0199.py:134 +msgid "XMPP PING received from {from_jid} [{profile}]" +msgstr "" + +#: sat/plugins/plugin_xep_0203.py:45 +#, fuzzy +msgid "Implementation of Delayed Delivery" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0203.py:51 +#, fuzzy +msgid "Delayed Delivery plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0231.py:48 +msgid "Implementation of bits of binary (used for small images/files)" +msgstr "" + +#: sat/plugins/plugin_xep_0231.py:59 +#, fuzzy +msgid "plugin Bits of Binary initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0234.py:54 +#, fuzzy +msgid "Implementation of Jingle File Transfer" +msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: sat/plugins/plugin_xep_0234.py:67 +#, fuzzy +msgid "file transfer" +msgstr "Transfert de fichier" + +#: sat/plugins/plugin_xep_0234.py:70 +#, fuzzy +msgid "plugin Jingle File Transfer initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0234.py:380 +msgid "hash_algo must be set if file_hash is set" +msgstr "" + +#: sat/plugins/plugin_xep_0234.py:385 +msgid "file_hash must be set if hash_algo is set" +msgstr "" + +#: sat/plugins/plugin_xep_0234.py:419 +msgid "only the following keys are allowed in extra: {keys}" +msgstr "" + +#: sat/plugins/plugin_xep_0234.py:454 +msgid "you need to provide at least name or file hash" +msgstr "" + +#: sat/plugins/plugin_xep_0234.py:524 +#, fuzzy +msgid "File continue is not implemented yet" +msgstr "getGame n'est pas implémenté dans ce frontend" + +#: sat/plugins/plugin_xep_0249.py:55 +#, fuzzy +msgid "Implementation of Direct MUC Invitations" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0249.py:75 +msgid "Auto-join MUC on invitation" +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:86 +#, fuzzy +msgid "Plugin XEP_0249 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0249.py:140 +#, python-format +msgid "Invitation accepted for room %(room)s [%(profile)s]" +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:155 +msgid "invalid invitation received: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:159 +#, python-format +msgid "Invitation received for room %(room)s [%(profile)s]" +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:170 +msgid "Invitation silently discarded because user is already in the room." +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:181 +#, python-format +msgid "" +"An invitation from %(user)s to join the room %(room)s has been declined " +"according to your personal settings." +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:185 sat/plugins/plugin_xep_0249.py:192 +#, fuzzy +msgid "MUC invitation" +msgstr "Connexion..." + +#: sat/plugins/plugin_xep_0249.py:188 +#, python-format +msgid "" +"You have been invited by %(user)s to join the room %(room)s. Do you " +"accept?" +msgstr "" + +#: sat/plugins/plugin_xep_0249.py:215 +msgid "You must provide a valid JID to invite, like in '/invite contact@{host}'" +msgstr "" + +#: sat/plugins/plugin_xep_0260.py:51 +#, fuzzy +msgid "Implementation of Jingle SOCKS5 Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: sat/plugins/plugin_xep_0260.py:64 +msgid "plugin Jingle SOCKS5 Bytestreams" +msgstr "" + +#: sat/plugins/plugin_xep_0261.py:47 +#, fuzzy +msgid "Implementation of Jingle In-Band Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: sat/plugins/plugin_xep_0261.py:55 +#, fuzzy +msgid "plugin Jingle In-Band Bytestreams" +msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" + +#: sat/plugins/plugin_xep_0264.py:67 +msgid "Thumbnails handling" +msgstr "" + +#: sat/plugins/plugin_xep_0264.py:79 +#, fuzzy +msgid "Plugin XEP_0264 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0277.py:71 +#, fuzzy +msgid "Implementation of microblogging Protocol" +msgstr "Implémentation du protocole de transports" + +#: sat/plugins/plugin_xep_0277.py:83 +#, fuzzy +msgid "Microblogging plugin initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0277.py:286 +msgid "Content of type XHTML must declare its namespace!" +msgstr "" + +#: sat/plugins/plugin_xep_0277.py:557 +msgid "Can't have xhtml and rich content at the same time" +msgstr "" + +#: sat/plugins/plugin_xep_0277.py:1041 +#, python-format +msgid "Microblog node has now access %s" +msgstr "" + +#: sat/plugins/plugin_xep_0277.py:1045 +msgid "Can't set microblog access" +msgstr "" + +#: sat/plugins/plugin_xep_0280.py:39 +#, fuzzy, python-format +msgid "Message carbons" +msgstr "message reçu de: %s" + +#: sat/plugins/plugin_xep_0280.py:50 +#, fuzzy +msgid "Implementation of Message Carbons" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0280.py:75 +#, fuzzy +msgid "Plugin XEP_0280 initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0280.py:102 +msgid "Not activating message carbons as requested in params" +msgstr "" + +#: sat/plugins/plugin_xep_0280.py:107 +msgid "server doesn't handle message carbons" +msgstr "" + +#: sat/plugins/plugin_xep_0280.py:109 +msgid "message carbons available, enabling it" +msgstr "" + +#: sat/plugins/plugin_xep_0280.py:117 +#, fuzzy +msgid "message carbons activated" +msgstr "" +"Barre de progression désactivée\n" +"--\n" + +#: sat/plugins/plugin_xep_0297.py:44 +#, fuzzy +msgid "Implementation of Stanza Forwarding" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0297.py:52 +#, fuzzy +msgid "Stanza Forwarding plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0300.py:45 +msgid "Management of cryptographic hashes" +msgstr "" + +#: sat/plugins/plugin_xep_0300.py:66 +#, fuzzy +msgid "plugin Hashes initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0313.py:51 +#, fuzzy +msgid "Implementation of Message Archive Management" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0313.py:64 +#, fuzzy +msgid "Message Archive Management plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0313.py:92 +msgid "It seems that we have no MAM history yet" +msgstr "" + +#: sat/plugins/plugin_xep_0313.py:126 +msgid "missing \"to\" attribute in forwarded message" +msgstr "" + +#: sat/plugins/plugin_xep_0313.py:137 +msgid "missing \"from\" attribute in forwarded message" +msgstr "" + +#: sat/plugins/plugin_xep_0313.py:140 +msgid "" +"was expecting a message sent by our jid, but this one if from {from_jid}," +" ignoring\n" +"{xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0313.py:158 +msgid "We have received no message while offline" +msgstr "" + +#: sat/plugins/plugin_xep_0313.py:160 +msgid "We have received {num_mess} message(s) while offline." +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:50 +#, fuzzy +msgid "Implementation of File Information Sharing" +msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: sat/plugins/plugin_xep_0329.py:86 +msgid "path change chars found in name [{name}], hack attempt?" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:107 +msgid "path can only be set on path nodes" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:141 +msgid "a node can't have several parents" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:210 +msgid "" +"parent dir (\"..\") found in path, hack attempt? path is {path} " +"[{profile}]" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:271 +#, fuzzy +msgid "File Information Sharing initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0329.py:394 +msgid "invalid path: {path}" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:428 +msgid "{peer_jid} requested a file (s)he can't access [{profile}]" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:461 +#, fuzzy, python-format +msgid "error while retrieving files: {msg}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat/plugins/plugin_xep_0329.py:513 +msgid "ignoring invalid unicode name ({name}): {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:534 +msgid "unexpected type: {type}" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:573 +#, fuzzy, python-format +msgid "unknown node type: {type}" +msgstr "Type d'action inconnu" + +#: sat/plugins/plugin_xep_0329.py:711 +msgid "unexpected element, ignoring: {elt}" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:1184 +#, fuzzy, python-format +msgid "This path doesn't exist!" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat/plugins/plugin_xep_0329.py:1186 +msgid "A path need to be specified" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:1188 +msgid "access must be a dict" +msgstr "" + +#: sat/plugins/plugin_xep_0329.py:1200 +#, fuzzy +msgid "Can't find a proper name" +msgstr "Impossible de trouver la VCard de %s" + +#: sat/plugins/plugin_xep_0329.py:1211 +msgid "" +"A directory with this name is already shared, renamed to {new_name} " +"[{profile}]" +msgstr "" + +#: sat/plugins/plugin_xep_0334.py:43 +#, fuzzy +msgid "Implementation of Message Processing Hints" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0334.py:45 +msgid "" +" Frontends can use HINT_* constants in mess_data['extra'] in " +"a serialized 'hints' dict.\n" +" Internal plugins can use directly addHint([HINT_* " +"constant]).\n" +" Will set mess_data['extra']['history'] to 'skipped' when no " +"store is requested and message is not saved in history." +msgstr "" + +#: sat/plugins/plugin_xep_0334.py:65 +#, fuzzy +msgid "Message Processing Hints plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0346.py:54 +msgid "Handle Pubsub data schemas" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:60 +#, fuzzy +msgid "PubSub Schema initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0346.py:208 +msgid "unspecified schema, we need to request it" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:212 +msgid "" +"no schema specified, and this node has no schema either, we can't " +"construct the data form" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:233 +msgid "Invalid Schema: {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:246 +msgid "nodeIndentifier needs to be set" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:310 +msgid "empty node is not allowed" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:354 +msgid "default_node must be set if nodeIdentifier is not set" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:457 +#, fuzzy +msgid "field {name} doesn't exist, ignoring it" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat/plugins/plugin_xep_0346.py:551 +msgid "Can't parse date field: {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:652 +msgid "Can't get previous item, update ignored: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:661 +msgid "Can't parse previous item, update ignored: data form not found" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:719 +msgid "default_node must be set if node is not set" +msgstr "" + +#: sat/plugins/plugin_xep_0346.py:728 +msgid "if extra[\"update\"] is set, item_id must be set too" +msgstr "" + +#: sat/plugins/plugin_xep_0352.py:35 +msgid "" +"Notify server when frontend is not actively used, to limit traffic and " +"save bandwidth and battery life" +msgstr "" + +#: sat/plugins/plugin_xep_0352.py:45 +#, fuzzy +msgid "Client State Indication plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0352.py:63 +msgid "Client State Indication is available on this server" +msgstr "" + +#: sat/plugins/plugin_xep_0352.py:67 +msgid "" +"Client State Indication is not available on this server, some bandwidth " +"optimisations can't be used." +msgstr "" + +#: sat/plugins/plugin_xep_0353.py:46 +#, fuzzy +msgid "Implementation of Jingle Message Initiation" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0353.py:53 +#, fuzzy +msgid "plugin {name} initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0353.py:122 +msgid "Message initiation with {peer_jid} timed out" +msgstr "" + +#: sat/plugins/plugin_xep_0353.py:166 +msgid "" +"Somebody not in your contact list ({peer_jid}) wants to do a " +"\"{human_name}\" session with you, this would leak your presence and " +"possibly you IP (internet localisation), do you accept?" +msgstr "" + +#: sat/plugins/plugin_xep_0353.py:171 +#, fuzzy +msgid "Invitation from an unknown contact" +msgstr "Tentative d'assigner un paramètre à un profile inconnu" + +#: sat/plugins/plugin_xep_0353.py:211 +msgid "no pending session found with id {session_id}, did it timed out?" +msgstr "" + +#: sat/plugins/plugin_xep_0359.py:40 +#, fuzzy +msgid "Implementation of Unique and Stable Stanza IDs" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0359.py:49 +#, fuzzy +msgid "Unique and Stable Stanza IDs plugin initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/plugins/plugin_xep_0363.py:51 +#, fuzzy +msgid "Implementation of HTTP File Upload" +msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " + +#: sat/plugins/plugin_xep_0363.py:83 +#, fuzzy +msgid "plugin HTTP File Upload initialization" +msgstr "Initialisation du plugin XEP_0054" + +#: sat/plugins/plugin_xep_0363.py:200 +msgid "Can't get upload slot: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0363.py:265 +msgid "upload failed: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0363.py:349 +msgid "Invalid header element: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0363.py:355 +msgid "Ignoring unauthorised header \"{name}\": {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0363.py:400 +msgid "no service can handle HTTP Upload request: {elt}" +msgstr "" + +#: sat/plugins/plugin_xep_0380.py:35 +#, fuzzy +msgid "Implementation of Explicit Message Encryption" +msgstr "Implémentation de l'enregistrement en ligne" + +#: sat/plugins/plugin_xep_0380.py:94 +msgid "" +"Message from {sender} is encrypted with {algorithm} and we can't decrypt " +"it." +msgstr "" + +#: sat/plugins/plugin_xep_0380.py:96 +msgid "" +"User {sender} sent you an encrypted message (encrypted with {algorithm})," +" and we can't decrypt it." +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:62 +#, fuzzy +msgid "Implementation of OMEMO" +msgstr "Implementation de vcard-temp" + +#: sat/plugins/plugin_xep_0384.py:440 +msgid "Security" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:442 +msgid "OMEMO default trust policy" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:443 +msgid "Manual trust (more secure)" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:445 +msgid "Blind Trust Before Verification (more user friendly)" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:449 +msgid "OMEMO plugin initialization (omemo module v{version})" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:453 +msgid "" +"Your version of omemo module is too old: {v[0]}.{v[1]}.{v[2]} is minimum " +"required, please update." +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:488 +msgid "You need to have OMEMO encryption activated to reset the session" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:503 +msgid "OMEMO session has been reset" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:551 +msgid "device {device} from {peer_jid} is not an auto-trusted device anymore" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:612 +msgid "Can't find bundle for device {device_id} of user {bare_jid}, ignoring" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:642 +#, fuzzy +msgid "OMEMO trust management" +msgstr "Initialisation du gestionnaire de mémoire" + +#: sat/plugins/plugin_xep_0384.py:645 +msgid "" +"This is OMEMO trusting system. You'll see below the devices of your " +"contacts, and a checkbox to trust them or not. A trusted device can read " +"your messages in plain text, so be sure to only validate devices that you" +" are sure are belonging to your contact. It's better to do this when you " +"are next to your contact and her/his device, so you can check the " +"\"fingerprint\" (the number next to the device) yourself. Do *not* " +"validate a device if the fingerprint is wrong!" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:655 +msgid "This device ID" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:657 +msgid "This device fingerprint" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:669 +msgid "Automatically trust new devices?" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:683 +#, fuzzy +msgid "Contact" +msgstr "&Contacts" + +#: sat/plugins/plugin_xep_0384.py:685 +msgid "Device ID" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:691 +msgid "Trust this device?" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:696 +msgid "(automatically trusted)" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:724 +msgid "We have no identity for this device yet, let's generate one" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:749 +msgid "Saving public bundle for this device ({device_id})" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:770 +msgid "OMEMO devices list is stored in more that one items, this is not expected" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:776 +msgid "no list element found in OMEMO devices list" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:782 +msgid "device element is missing \"id\" attribute: {elt}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:785 +msgid "invalid device id: {device_id}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:804 +msgid "there is no node to handle OMEMO devices" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:826 +msgid "Can't set devices: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:850 +msgid "Bundle missing for device {device_id}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:855 +msgid "Can't get bundle for device {device_id}: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:859 +msgid "" +"no item found in node {node}, can't get public bundle for device " +"{device_id}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:864 +msgid "more than one item found in {node}, this is not expected" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:878 +msgid "invalid bundle for device {device_id}, ignoring" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:902 +msgid "error while decoding key for device {device_id}: {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:916 +msgid "updating bundle for {device_id}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:949 +msgid "Can't set bundle: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:969 +msgid "Our own device is missing from devices list, fixing it" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:988 +msgid "" +"Not all destination devices are trusted, unknown devices will be blind " +"trusted due to the OMEMO Blind Trust Before Verification policy. If you " +"want a more secure workflow, please activate \"manual\" OMEMO policy in " +"settings' \"Security\" tab.\n" +"Following fingerprint have been automatically trusted:\n" +"{devices}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1010 +msgid "" +"Not all destination devices are trusted, we can't encrypt message in such" +" a situation. Please indicate if you trust those devices or not in the " +"trust manager before we can send this message" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1053 +msgid "discarding untrusted device {device_id} with key {device_key} for {entity}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1095 +msgid "" +"Can't retrieve bundle for device(s) {devices} of entity {peer}, the " +"message will not be readable on this/those device(s)" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1100 +msgid "" +"You're destinee {peer} has missing encryption data on some of his/her " +"device(s) (bundle on device {devices}), the message won't be readable on" +" this/those device." +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1151 +msgid "Too many iterations in encryption loop" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1180 +msgid "Can't encrypt message for {entities}: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1270 +msgid "Invalid OMEMO encrypted stanza, ignoring: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1276 +msgid "Invalid OMEMO encrypted stanza, missing sender device ID, ignoring: {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1284 +msgid "" +"This OMEMO encrypted stanza has not been encrypted for our device " +"(device_id: {device_id}, fingerprint: {fingerprint}): {xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1290 +msgid "" +"An OMEMO message from {sender} has not been encrypted for our device, we " +"can't decrypt it" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1297 +msgid "Invalid recipient ID: {msg}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1330 +msgid "" +"Can't decrypt message: {reason}\n" +"{xml}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1332 +msgid "An OMEMO message from {sender} can't be decrypted: {reason}" +msgstr "" + +#: sat/plugins/plugin_xep_0384.py:1364 +msgid "" +"Our message with UID {uid} has not been received in time, it has probably" +" been lost. The message was: {msg!r}" +msgstr "" + +#: sat/plugins/plugin_app_manager_docker/__init__.py:38 +msgid "Applications Manager for Docker" +msgstr "" + +#: sat/plugins/plugin_app_manager_docker/__init__.py:48 +#, fuzzy +msgid "Docker App Manager initialization" +msgstr "Initialisation de l'extension pour les transports" + +#: sat/stdui/ui_contact_list.py:39 sat/stdui/ui_contact_list.py:42 +#: sat/stdui/ui_contact_list.py:190 sat/stdui/ui_contact_list.py:276 +#, fuzzy +msgid "Add contact" +msgstr "&Ajouter un contact" + +#: sat/stdui/ui_contact_list.py:45 sat/stdui/ui_contact_list.py:48 +#: sat/stdui/ui_contact_list.py:209 +#, fuzzy +msgid "Update contact" +msgstr "&Ajouter un contact" + +#: sat/stdui/ui_contact_list.py:51 sat/stdui/ui_contact_list.py:54 +#, fuzzy +msgid "Remove contact" +msgstr "Supp&rimer un contact" + +#: sat/stdui/ui_contact_list.py:157 +msgid "Select in which groups your contact is:" +msgstr "" + +#: sat/stdui/ui_contact_list.py:172 +msgid "Add group" +msgstr "" + +#: sat/stdui/ui_contact_list.py:174 +msgid "Add" +msgstr "" + +#: sat/stdui/ui_contact_list.py:191 +#, fuzzy, python-format +msgid "New contact identifier (JID):" +msgstr "nouveau contact: %s" + +#: sat/stdui/ui_contact_list.py:203 +msgid "Nothing to update" +msgstr "" + +#: sat/stdui/ui_contact_list.py:204 sat/stdui/ui_contact_list.py:223 +msgid "Your contact list is empty." +msgstr "" + +#: sat/stdui/ui_contact_list.py:210 +msgid "Which contact do you want to update?" +msgstr "" + +#: sat/stdui/ui_contact_list.py:222 +msgid "Nothing to delete" +msgstr "" + +#: sat/stdui/ui_contact_list.py:228 +#, fuzzy, python-format +msgid "Who do you want to remove from your contacts?" +msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" + +#: sat/stdui/ui_contact_list.py:251 +#, fuzzy +msgid "Delete contact" +msgstr "&Ajouter un contact" + +#: sat/stdui/ui_contact_list.py:253 +#, fuzzy, python-format +msgid "Are you sure you want to remove %s from your contact list?" +msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" + +#: sat/stdui/ui_contact_list.py:277 +#, python-format +msgid "Please enter a valid JID (like \"contact@%s\"):" +msgstr "" + +#: sat/stdui/ui_profile_manager.py:62 +msgid "Profile password for {}" +msgstr "" + +#: sat/stdui/ui_profile_manager.py:72 sat/stdui/ui_profile_manager.py:119 +#, fuzzy +msgid "Connection error" +msgstr "Connexion..." + +#: sat/stdui/ui_profile_manager.py:76 +#: sat_frontends/quick_frontend/quick_profile_manager.py:171 +#, fuzzy +msgid "Internal error" +msgstr "Transfert de fichier" + +#: sat/stdui/ui_profile_manager.py:77 +msgid "Internal error: {}" +msgstr "" + +#: sat/stdui/ui_profile_manager.py:121 +#, python-format +msgid "Can't connect to %s. Please check your connection details." +msgstr "" + +#: sat/stdui/ui_profile_manager.py:127 +#, python-format +msgid "XMPP password for %(profile)s%(counter)s" +msgstr "" + +#: sat/stdui/ui_profile_manager.py:135 +#, python-format +msgid "" +"Can't connect to %s. Please check your connection details or try with " +"another password." +msgstr "" + +#: sat/test/constants.py:57 +msgid "Enable unibox" +msgstr "" + +#: sat/test/constants.py:58 +msgid "'Wysiwyg' edition" +msgstr "" + +#: sat/test/test_plugin_misc_room_game.py:43 +msgid "Dummy plugin to test room game" +msgstr "" + +#: sat/tools/config.py:53 +#, fuzzy, python-format +msgid "Testing file %s" +msgstr "Échec de l'inscription: %s" + +#: sat/tools/config.py:72 +msgid "Config auto-update: {option} set to {value} in the file {config_file}." +msgstr "" + +#: sat/tools/config.py:86 +msgid "Can't read main config: {msg}" +msgstr "" + +#: sat/tools/config.py:91 +msgid "Configuration was read from: {filenames}" +msgstr "" + +#: sat/tools/config.py:95 +#, fuzzy, python-format +msgid "No configuration file found, using default settings" +msgstr "Disposition inconnue, utilisation de celle par defaut" + +#: sat/tools/image.py:35 +msgid "SVG support not available, please install cairosvg: {e}" +msgstr "" + +#: sat/tools/trigger.py:66 +#, python-format +msgid "There is already a bound priority [%s]" +msgstr "" + +#: sat/tools/trigger.py:69 +#, python-format +msgid "There is already a trigger with the same priority [%s]" +msgstr "" + +#: sat/tools/video.py:38 +msgid "ffmpeg executable not found, video thumbnails won't be available" +msgstr "" + +#: sat/tools/video.py:56 +msgid "ffmpeg executable is not available, can't generate video thumbnail" +msgstr "" + +#: sat/tools/xml_tools.py:86 +msgid "Fixed field has neither value nor label, ignoring it" +msgstr "" + +#: sat/tools/xml_tools.py:485 +#, fuzzy +msgid "INTERNAL ERROR: parameters xml not valid" +msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" + +#: sat/tools/xml_tools.py:495 +msgid "INTERNAL ERROR: params categories must have a name" +msgstr "ERREUR INTERNE: les catégories des paramètres doivent avoir un nom" + +#: sat/tools/xml_tools.py:505 +msgid "INTERNAL ERROR: params must have a name" +msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" + +#: sat/tools/xml_tools.py:557 +msgid "The 'options' tag is not allowed in parameter of type 'list'!" +msgstr "" + +#: sat/tools/xml_tools.py:655 +msgid "TabElement must be a child of TabsContainer" +msgstr "" + +#: sat/tools/xml_tools.py:760 +msgid "Can't set row index if auto_index is True" +msgstr "" + +#: sat/tools/xml_tools.py:893 +msgid "either items or columns need do be filled" +msgstr "" + +#: sat/tools/xml_tools.py:907 +msgid "Headers lenght doesn't correspond to columns" +msgstr "" + +#: sat/tools/xml_tools.py:954 +msgid "Incorrect number of items in list" +msgstr "" + +#: sat/tools/xml_tools.py:978 +#, fuzzy +msgid "A widget with the name \"{name}\" already exists." +msgstr "Ce nom de profile existe déjà" + +#: sat/tools/xml_tools.py:1171 +msgid "Value must be an integer" +msgstr "" + +#: sat/tools/xml_tools.py:1186 +msgid "Value must be 0, 1, false or true" +msgstr "" + +#: sat/tools/xml_tools.py:1249 +msgid "" +"\"multi\" flag and \"selected\" option are not compatible with " +"\"noselect\" flag" +msgstr "" + +#: sat/tools/xml_tools.py:1258 +msgid "empty \"options\" list" +msgstr "" + +#: sat/tools/xml_tools.py:1277 sat/tools/xml_tools.py:1311 +msgid "invalid styles" +msgstr "" + +#: sat/tools/xml_tools.py:1335 +msgid "DialogElement must be a direct child of TopElement" +msgstr "" + +#: sat/tools/xml_tools.py:1350 +msgid "MessageElement must be a direct child of DialogElement" +msgstr "" + +#: sat/tools/xml_tools.py:1365 +msgid "ButtonsElement must be a direct child of DialogElement" +msgstr "" + +#: sat/tools/xml_tools.py:1379 +msgid "FileElement must be a direct child of DialogElement" +msgstr "" + +#: sat/tools/xml_tools.py:1458 +#, fuzzy, python-format +msgid "Unknown panel type [%s]" +msgstr "Type d'action inconnu" + +#: sat/tools/xml_tools.py:1460 +msgid "form XMLUI need a submit_id" +msgstr "" + +#: sat/tools/xml_tools.py:1462 +msgid "container argument must be a string" +msgstr "" + +#: sat/tools/xml_tools.py:1465 +msgid "dialog_opt can only be used with dialog panels" +msgstr "" + +#: sat/tools/xml_tools.py:1492 +msgid "createWidget can't be used with dialogs" +msgstr "" + +#: sat/tools/xml_tools.py:1590 +msgid "Submit ID must be filled for this kind of dialog" +msgstr "" + +#: sat/tools/xml_tools.py:1618 +#, fuzzy, python-format +msgid "Unknown container type [%s]" +msgstr "Type d'action inconnu" + +#: sat/tools/xml_tools.py:1648 +#, fuzzy, python-format +msgid "Invalid type [{type_}]" +msgstr "Type d'action inconnu" + +#: sat/tools/common/async_process.py:86 +msgid "" +"Can't complete {name} command (error code: {code}):\n" +"stderr:\n" +"{stderr}\n" +"{stdout}\n" +msgstr "" + +#: sat/tools/common/date_utils.py:76 +msgid "You can't use a direction (+ or -) and \"ago\" at the same time" +msgstr "" + +#: sat/tools/common/template.py:149 +msgid "{site} can't be used as site name, it's reserved." +msgstr "" + +#: sat/tools/common/template.py:157 +msgid "{theme} contain forbidden char. Following chars are forbidden: {reserved}" +msgstr "" + +#: sat/tools/common/template.py:212 +msgid "Unregistered site requested: {site_to_check}" +msgstr "" + +#: sat/tools/common/template.py:241 +msgid "" +"Absolute template used while unsecure is disabled, hack attempt? " +"Template: {template}" +msgstr "" + +#: sat/tools/common/template.py:314 +msgid "Invalid attribute, please use one of \"defer\", \"async\" or \"\"" +msgstr "" + +#: sat/tools/common/template.py:332 +msgid "Can't find {libary} javascript library" +msgstr "" + +#: sat/tools/common/template.py:389 +msgid "" +"Can't add \"{name}\" site, it contains forbidden characters. Forbidden " +"characters are {forbidden}." +msgstr "" + +#: sat/tools/common/template.py:395 +msgid "Can't add \"{name}\" site, it should map to an absolute path" +msgstr "" + +#: sat/tools/common/template.py:416 +msgid "Can't load theme settings at {path}: {e}" +msgstr "" + +#: sat/tools/common/template.py:523 +msgid "Can't find template translation at {path}" +msgstr "" + +#: sat/tools/common/template.py:526 +msgid "{site}Invalid locale name: {msg}" +msgstr "" + +#: sat/tools/common/template.py:529 +msgid "{site}loaded {lang} templates translations" +msgstr "" + +#: sat/tools/common/template.py:560 +msgid "invalid locale value: {msg}" +msgstr "" + +#: sat/tools/common/template.py:569 +msgid "Can't find locale {locale}" +msgstr "" + +#: sat/tools/common/template.py:574 +msgid "Switched to {lang}" +msgstr "" + +#: sat/tools/common/template.py:774 sat_frontends/jp/cmd_event.py:134 +msgid "Can't parse date: {msg}" +msgstr "" + +#: sat/tools/common/template.py:801 +#, fuzzy +msgid "ignoring field \"{name}\": it doesn't exists" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/arg_tools.py:88 +msgid "ignoring {name}={value}, not corresponding to any argument (in USE)" +msgstr "" + +#: sat_frontends/jp/arg_tools.py:95 +msgid "arg {name}={value} (in USE)" +msgstr "" + +#: sat_frontends/jp/base.py:64 +#, fuzzy +msgid "" +"ProgressBar not available, please download it at " +"http://pypi.python.org/pypi/progressbar\n" +"Progress bar deactivated\n" +"--\n" +msgstr "" +"ProgressBar n'est pas disponible, veuillez le télécharger à " +"http://pypi.python.org/pypi/progressbar" + +#: sat_frontends/jp/base.py:155 +msgid "" +"Invalid value set for \"background\" ({background}), please check your " +"settings in libervia.conf" +msgstr "" + +#: sat_frontends/jp/base.py:178 +msgid "Available commands" +msgstr "" + +#: sat_frontends/jp/base.py:287 +#, python-format +msgid "Use PROFILE profile key (default: %(default)s)" +msgstr "" + +#: sat_frontends/jp/base.py:290 +msgid "Password used to connect profile, if necessary" +msgstr "" + +#: sat_frontends/jp/base.py:297 +msgid "Connect the profile before doing anything else" +msgstr "" + +#: sat_frontends/jp/base.py:307 +msgid "Start a profile session without connecting" +msgstr "" + +#: sat_frontends/jp/base.py:313 +msgid "Show progress bar" +msgstr "Affiche la barre de progression" + +#: sat_frontends/jp/base.py:318 +msgid "Add a verbosity level (can be used multiple times)" +msgstr "" + +#: sat_frontends/jp/base.py:323 +msgid "be quiet (only output machine readable data)" +msgstr "" + +#: sat_frontends/jp/base.py:326 +msgid "draft handling" +msgstr "" + +#: sat_frontends/jp/base.py:328 +msgid "load current draft" +msgstr "" + +#: sat_frontends/jp/base.py:330 +msgid "path to a draft file to retrieve" +msgstr "" + +#: sat_frontends/jp/base.py:346 +msgid "Pubsub URL (xmpp or http)" +msgstr "" + +#: sat_frontends/jp/base.py:348 +#, fuzzy +msgid "JID of the PubSub service" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/base.py:350 +msgid "PEP service" +msgstr "" + +#: sat_frontends/jp/base.py:352 sat_frontends/jp/base.py:360 +#: sat_frontends/jp/base.py:368 +msgid " (DEFAULT: {default})" +msgstr "" + +#: sat_frontends/jp/base.py:356 +#, fuzzy +msgid "node to request" +msgstr "Demande de suppression de contact" + +#: sat_frontends/jp/base.py:358 +msgid "standard node" +msgstr "" + +#: sat_frontends/jp/base.py:366 +msgid "last item" +msgstr "" + +#: sat_frontends/jp/base.py:372 +msgid "retrieve last item" +msgstr "" + +#: sat_frontends/jp/base.py:378 +msgid "items to retrieve (DEFAULT: all)" +msgstr "" + +#: sat_frontends/jp/base.py:385 +msgid "maximum number of items to get ({no_limit} to get all items)" +msgstr "" + +#: sat_frontends/jp/base.py:391 +msgid "maximum number of items to get per page (DEFAULT: 10)" +msgstr "" + +#: sat_frontends/jp/base.py:398 sat_frontends/jp/cmd_message.py:217 +msgid "find page after this item" +msgstr "" + +#: sat_frontends/jp/base.py:401 sat_frontends/jp/cmd_message.py:220 +msgid "find page before this item" +msgstr "" + +#: sat_frontends/jp/base.py:404 sat_frontends/jp/cmd_message.py:223 +msgid "index of the page to retrieve" +msgstr "" + +#: sat_frontends/jp/base.py:411 +#, fuzzy +msgid "MAM filters to use" +msgstr "Veuillez choisir le fichier à envoyer" + +#: sat_frontends/jp/base.py:424 +msgid "how items should be ordered" +msgstr "" + +#: sat_frontends/jp/base.py:454 +msgid "there is already a default output for {type}, ignoring new one" +msgstr "" + +#: sat_frontends/jp/base.py:475 +msgid "The following output options are invalid: {invalid_options}" +msgstr "" + +#: sat_frontends/jp/base.py:499 +msgid "Can't import {module_path} plugin, ignoring it: {e}" +msgstr "" + +#: sat_frontends/jp/base.py:505 +msgid "Missing module for plugin {name}: {missing}" +msgstr "" + +#: sat_frontends/jp/base.py:520 +msgid "Invalid plugin module [{type}] {module}" +msgstr "" + +#: sat_frontends/jp/base.py:552 +msgid "Can't parse HTML page : {msg}" +msgstr "" + +#: sat_frontends/jp/base.py:558 +msgid "" +"Could not find alternate \"xmpp:\" URI, can't find associated XMPP PubSub" +" node/item" +msgstr "" + +#: sat_frontends/jp/base.py:576 +msgid "invalid XMPP URL: {url}" +msgstr "" + +#: sat_frontends/jp/base.py:596 +msgid "item specified in URL but not needed in command, ignoring it" +msgstr "" + +#: sat_frontends/jp/base.py:612 +msgid "XMPP URL is not a pubsub one: {url}" +msgstr "" + +#: sat_frontends/jp/base.py:618 +msgid "argument -s/--service is required" +msgstr "" + +#: sat_frontends/jp/base.py:620 +msgid "argument -n/--node is required" +msgstr "" + +#: sat_frontends/jp/base.py:622 +msgid "argument -i/--item is required" +msgstr "" + +#: sat_frontends/jp/base.py:629 +msgid "--item and --item-last can't be used at the same time" +msgstr "" + +#: sat_frontends/jp/base.py:659 sat_frontends/quick_frontend/quick_app.py:370 +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é ?" + +#: sat_frontends/jp/base.py:662 sat_frontends/quick_frontend/quick_app.py:373 +#, fuzzy +msgid "Can't init bridge" +msgstr "Construction du jeu de Tarot" + +#: sat_frontends/jp/base.py:666 +msgid "Error while initialising bridge: {e}" +msgstr "" + +#: sat_frontends/jp/base.py:714 +msgid "action cancelled by user" +msgstr "" + +#: sat_frontends/jp/base.py:785 +#, python-format +msgid "%s is not a valid JID !" +msgstr "%s n'est pas un JID valide !" + +#: sat_frontends/jp/base.py:837 +#, fuzzy +msgid "invalid password" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat_frontends/jp/base.py:839 +#, fuzzy +msgid "please enter profile password:" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/jp/base.py:859 +#, fuzzy, python-format +msgid "The profile [{profile}] doesn't exist" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/base.py:881 +#, fuzzy, python-format +msgid "" +"Session for [{profile}] is not started, please start it before using jp, " +"or use either --start-session or --connect option" +msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" + +#: sat_frontends/jp/base.py:901 +#, fuzzy, python-format +msgid "" +"Profile [{profile}] is not connected, please connect it before using jp, " +"or use --connect option" +msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" + +#: sat_frontends/jp/base.py:1002 +msgid "select output format (default: {})" +msgstr "" + +#: sat_frontends/jp/base.py:1005 +msgid "output specific option" +msgstr "" + +#: sat_frontends/jp/base.py:1111 +msgid "file size is not known, we can't show a progress bar" +msgstr "" + +#: sat_frontends/jp/base.py:1126 sat_frontends/jp/cmd_list.py:304 +msgid "Progress: " +msgstr "Progression: " + +#: sat_frontends/jp/base.py:1156 +#, fuzzy +msgid "Operation started" +msgstr "inscription demandée pour" + +#: sat_frontends/jp/base.py:1172 +#, fuzzy, python-format +msgid "Operation successfully finished" +msgstr "Transfert [%s] refusé" + +#: sat_frontends/jp/base.py:1179 +msgid "Error while doing operation: {e}" +msgstr "" + +#: sat_frontends/jp/base.py:1189 +msgid "trying to use output when use_output has not been set" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:42 +msgid "create a XMPP account" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:47 +msgid "jid to create" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:50 +msgid "password of the account" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:55 +msgid "create a profile to use this account (default: don't create profile)" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:63 +msgid "email (usage depends of XMPP server)" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:69 +msgid "server host (IP address or domain, default: use localhost)" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:76 +msgid "server port (default: {port})" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:107 +msgid "XMPP account created" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:113 +#, fuzzy +msgid "creating profile" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/jp/cmd_account.py:129 +msgid "Can't create profile {profile} to associate with jid {jid}: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:142 +#, fuzzy +msgid "profile created" +msgstr "Aucun profile sélectionné" + +#: sat_frontends/jp/cmd_account.py:183 +msgid "change password for XMPP account" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:188 +#, fuzzy +msgid "new XMPP password" +msgstr "Sauvegarde du nouveau mot de passe" + +#: sat_frontends/jp/cmd_account.py:207 +msgid "delete a XMPP account" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:215 +msgid "delete account without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_account.py:236 +msgid "Account deletion cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:34 +#, fuzzy +msgid "remote control a software" +msgstr "Supp&rimer un contact" + +#: sat_frontends/jp/cmd_adhoc.py:38 +msgid "software name" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:44 +msgid "jids allowed to use the command" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:51 +msgid "groups allowed to use the command" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:57 +msgid "groups that are *NOT* allowed to use the command" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:63 +msgid "jids that are *NOT* allowed to use the command" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:66 +#, fuzzy +msgid "loop on the commands" +msgstr "Mauvais nom de profile" + +#: sat_frontends/jp/cmd_adhoc.py:93 +#, fuzzy, python-format +msgid "No bus name found" +msgstr "Fonctionnalité trouvée: %s" + +#: sat_frontends/jp/cmd_adhoc.py:96 +#, fuzzy, python-format +msgid "Bus name found: [%s]" +msgstr "Fonctionnalité trouvée: %s" + +#: sat_frontends/jp/cmd_adhoc.py:100 +msgid "Command found: (path:{path}, iface: {iface}) [{command}]" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:112 +msgid "run an Ad-Hoc command" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:120 sat_frontends/jp/cmd_message.py:200 +msgid "jid of the service (default: profile's server" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:128 +msgid "submit form/page" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:137 +msgid "field value" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:143 +msgid "node of the command (default: list commands)" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:171 +msgid "list Ad-Hoc commands of a service" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:179 +msgid "jid of the service (default: profile's server)" +msgstr "" + +#: sat_frontends/jp/cmd_adhoc.py:202 +msgid "Ad-hoc commands" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:33 +msgid "list available applications" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:45 +msgid "show applications with this status" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:72 +#, fuzzy +msgid "start an application" +msgstr "Sélection du contrat" + +#: sat_frontends/jp/cmd_application.py:78 +msgid "name of the application to start" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:98 +msgid "stop a running application" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:106 +msgid "name of the application to stop" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:111 +msgid "identifier of the instance to stop" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:142 +msgid "show data exposed by a running application" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:150 +msgid "name of the application to check" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:155 +msgid "identifier of the instance to check" +msgstr "" + +#: sat_frontends/jp/cmd_application.py:189 +#, fuzzy +msgid "manage applications" +msgstr "Tab inconnu" + +#: sat_frontends/jp/cmd_avatar.py:38 +msgid "retrieve avatar of an entity" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:43 sat_frontends/jp/cmd_identity.py:42 +msgid "do no use cached values" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:46 +msgid "show avatar" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:48 sat_frontends/jp/cmd_info.py:111 +#, fuzzy +msgid "entity" +msgstr "Petite" + +#: sat_frontends/jp/cmd_avatar.py:87 +#, fuzzy +msgid "No avatar found." +msgstr "Aucune donnée trouvée" + +#: sat_frontends/jp/cmd_avatar.py:103 +msgid "set avatar of the profile or an entity" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:108 +msgid "entity whose avatar must be changed" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:110 +msgid "path to the image to upload" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:116 +#, fuzzy, python-format +msgid "file {path} doesn't exist!" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/cmd_avatar.py:125 +msgid "avatar has been set" +msgstr "" + +#: sat_frontends/jp/cmd_avatar.py:134 +msgid "avatar uploading/retrieving" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:129 +msgid "unknown syntax requested ({syntax})" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:138 +#, fuzzy +msgid "title of the item" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_blog.py:143 +msgid "tag (category) of your item" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:148 +msgid "language of the item (ISO 639 code)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:158 +msgid "" +"enable comments (default: comments not enabled except if they already " +"exist)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:168 +msgid "disable comments (will remove comments node if it exist)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:174 +msgid "syntax to use (default: get profile's default syntax)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:211 +msgid "publish a new blog item or update an existing one" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:257 +msgid "get blog item(s)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:267 +msgid "microblog data key(s) to display (default: depend of verbosity)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:462 +msgid "edit an existing or new blog post" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:473 +msgid "launch a blog preview in parallel" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:478 +msgid "add \"publish: False\" to metadata" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:628 +msgid "You need lxml to edit pretty XHTML" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:662 +msgid "rename an blog item" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:666 sat_frontends/jp/cmd_pubsub.py:996 +msgid "new item id to use" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:690 +msgid "preview a blog content" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:700 +msgid "use inotify to handle preview" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:706 +#, fuzzy +msgid "path to the content file" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_blog.py:810 +#, fuzzy, python-format +msgid "File \"{file}\" doesn't exist!" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/cmd_blog.py:898 +msgid "import an external blog" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:905 sat_frontends/jp/cmd_list.py:207 +msgid "importer name, nothing to display importers list" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:907 +msgid "original blog host" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:911 +msgid "do *NOT* upload images (default: do upload images)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:915 +msgid "do not upload images from this host (default: upload all images)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:920 +msgid "ignore invalide TLS certificate for uploads" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:929 sat_frontends/jp/cmd_list.py:216 +msgid "importer specific options (see importer description)" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:934 sat_frontends/jp/cmd_list.py:250 +msgid "" +"importer data location (see importer description), nothing to show " +"importer description" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:941 +msgid "Blog upload started" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:944 +msgid "Blog uploaded successfully" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:965 +msgid "" +"\n" +"To redirect old URLs to new ones, put the following lines in your " +"sat.conf file, in [libervia] section:\n" +"\n" +"{conf}" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:973 +#, fuzzy, python-format +msgid "Error while uploading blog: {error_msg}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_blog.py:982 sat_frontends/jp/cmd_list.py:274 +msgid "{name} argument can't be used without location argument" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:1037 +msgid "Error while trying to import a blog: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_blog.py:1050 +msgid "blog/microblog management" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:40 +#, python-format +msgid "storage location (default: %(default)s)" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:48 +#, python-format +msgid "bookmarks type (default: %(default)s)" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:54 +msgid "list bookmarks" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:94 +msgid "remove a bookmark" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:99 sat_frontends/jp/cmd_bookmarks.py:131 +msgid "jid (for muc bookmark) or url of to remove" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:105 +msgid "delete bookmark without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:110 +#, fuzzy, python-format +msgid "Are you sure to delete this bookmark?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/jp/cmd_bookmarks.py:117 +msgid "can't delete bookmark: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:120 +msgid "bookmark deleted" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:133 +msgid "bookmark name" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:134 +msgid "MUC specific options" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:135 +#, fuzzy +msgid "nickname" +msgstr "Surnon" + +#: sat_frontends/jp/cmd_bookmarks.py:140 +msgid "join room on profile connection" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:145 +msgid "You can't use --autojoin or --nick with --type url" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:165 +msgid "bookmark successfully added" +msgstr "" + +#: sat_frontends/jp/cmd_bookmarks.py:174 +msgid "manage bookmarks" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:49 +msgid "call a bridge method" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:54 +msgid "name of the method to execute" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:56 +msgid "argument of the method" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:79 +msgid "Error while executing {method}: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:94 +msgid "send a fake signal from backend" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:99 +msgid "name of the signal to send" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:100 +msgid "argument of the signal" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:112 +#, fuzzy, python-format +msgid "Can't send fake signal: {e}" +msgstr "message reçu de: %s" + +#: sat_frontends/jp/cmd_debug.py:123 +msgid "bridge s(t)imulation" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:135 +msgid "monitor XML stream" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:144 +msgid "stream direction filter" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:195 +msgid "print colours used with your background" +msgstr "" + +#: sat_frontends/jp/cmd_debug.py:226 +msgid "debugging tools" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:38 +msgid "show available encryption algorithms" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:45 +msgid "No encryption plugin registered!" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:47 +msgid "Following encryption algorithms are available: {algos}" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:68 +msgid "get encryption session data" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:73 +msgid "jid of the entity to check" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:99 +msgid "start encrypted session with an entity" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:105 sat_frontends/jp/cmd_message.py:77 +msgid "don't replace encryption algorithm if an other one is already used" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:108 +msgid "algorithm name (DEFAULT: choose automatically)" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:111 +msgid "algorithm namespace (DEFAULT: choose automatically)" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:114 +#: sat_frontends/jp/cmd_encryption.py:153 +#: sat_frontends/jp/cmd_encryption.py:178 +msgid "jid of the entity to stop encrypted session with" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:148 +msgid "stop encrypted session with an entity" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:173 +msgid "get UI to manage trust" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:182 +msgid "algorithm name (DEFAULT: current algorithm)" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:185 +msgid "algorithm namespace (DEFAULT: current algorithm)" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:220 +msgid "trust manangement" +msgstr "" + +#: sat_frontends/jp/cmd_encryption.py:230 +msgid "encryption sessions handling" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:47 +msgid "get list of registered events" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:78 +msgid "get event data" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:108 +#, fuzzy +msgid "ID of the PubSub Item" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_event.py:110 +msgid "date of the event" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:118 sat_frontends/jp/cmd_event.py:257 +#: sat_frontends/jp/cmd_pubsub.py:129 +#, fuzzy +msgid "configuration field to set" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_event.py:150 +msgid "create or replace event" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:170 +msgid "Event created successfuly on node {node}" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:181 +msgid "modify an existing event" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:214 sat_frontends/jp/cmd_event.py:288 +msgid "get event attendance" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:219 +#, fuzzy +msgid "bare jid of the invitee" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_event.py:246 +msgid "set event attendance" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:296 +msgid "show missing people (invited but no R.S.V.P. so far)" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:302 +msgid "don't show people which gave R.S.V.P." +msgstr "" + +#: sat_frontends/jp/cmd_event.py:371 +msgid "Attendees: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:374 +msgid " (" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:376 +msgid "yes: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:379 +msgid ", maybe: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:383 +msgid "no: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:390 +msgid "confirmed guests: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:395 +msgid "unconfirmed guests: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:401 +msgid "total: " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:408 +msgid "missing people (no reply): " +msgstr "" + +#: sat_frontends/jp/cmd_event.py:416 +msgid "you need to use --missing if you use --no-rsvp" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:489 +msgid "invite someone to the event through email" +msgstr "" + +#: sat_frontends/jp/cmd_event.py:568 +#, fuzzy +msgid "manage invities" +msgstr "Initialisation du gestionnaire de mémoire" + +#: sat_frontends/jp/cmd_event.py:577 +msgid "event management" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:50 +#, fuzzy +msgid "send a file to a contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat_frontends/jp/cmd_file.py:55 +#, fuzzy +msgid "a list of file" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_file.py:57 sat_frontends/jp/cmd_file.py:191 +#: sat_frontends/jp/cmd_message.py:82 sat_frontends/jp/cmd_pipe.py:42 +msgid "the destination jid" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:59 +#, fuzzy +msgid "make a bzip2 tarball" +msgstr "Fait un fichier compressé bzip2" + +#: sat_frontends/jp/cmd_file.py:79 sat_frontends/jp/cmd_file.py:236 +#: sat_frontends/jp/cmd_file.py:330 +msgid "File copy started" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:82 +#, fuzzy +msgid "File sent successfully" +msgstr "Inscription réussie" + +#: sat_frontends/jp/cmd_file.py:86 +#, fuzzy +msgid "The file has been refused by your contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat_frontends/jp/cmd_file.py:88 +#, fuzzy, python-format +msgid "Error while sending file: {}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_file.py:97 +msgid "File request sent to {jid}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:102 +#, fuzzy +msgid "Can't send file to {jid}" +msgstr "Impossible de trouver la VCard de %s" + +#: sat_frontends/jp/cmd_file.py:109 +#, fuzzy, python-format +msgid "file {file_} doesn't exist!" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/cmd_file.py:114 +#, fuzzy, python-format +msgid "{file_} 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." + +#: sat_frontends/jp/cmd_file.py:129 +#, fuzzy +msgid "bz2 is an experimental option, use with caution" +msgstr "" +"bz2 est une option expérimentale à un stade de développement peu avancé, " +"utilisez-là avec prudence" + +#: sat_frontends/jp/cmd_file.py:131 +msgid "Starting compression, please wait..." +msgstr "Lancement de la compression, veuillez patienter..." + +#: sat_frontends/jp/cmd_file.py:138 +#, fuzzy, python-format +msgid "Adding {}" +msgstr "Ajout de %s" + +#: sat_frontends/jp/cmd_file.py:141 +#, fuzzy +msgid "Done !" +msgstr "N° de Tél:" + +#: sat_frontends/jp/cmd_file.py:183 +#, fuzzy +msgid "request a file from a contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat_frontends/jp/cmd_file.py:195 +msgid "" +"destination path where the file will be saved (default: " +"[current_dir]/[name|hash])" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:204 +#, fuzzy +msgid "name of the file" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_file.py:210 +#, fuzzy +msgid "hash of the file" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_file.py:216 +msgid "hash algorithm use for --hash (default: sha-256)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:232 sat_frontends/jp/cmd_file.py:476 +msgid "overwrite existing file without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:239 sat_frontends/jp/cmd_file.py:333 +#, fuzzy, python-format +msgid "File received successfully" +msgstr "tarot: chien reçu" + +#: sat_frontends/jp/cmd_file.py:243 +msgid "The file request has been refused" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:245 +#, fuzzy, python-format +msgid "Error while requesting file: {}" +msgstr "Échec de la désinscription: %s" + +#: sat_frontends/jp/cmd_file.py:249 +msgid "at least one of --name or --hash must be provided" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:258 sat_frontends/jp/cmd_file.py:510 +msgid "File {path} already exists! Do you want to overwrite?" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:261 +msgid "file request cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:280 +#, fuzzy, python-format +msgid "can't request file: {e}" +msgstr "Échec de la désinscription: %s" + +#: sat_frontends/jp/cmd_file.py:293 +#, fuzzy +msgid "wait for a file to be sent by a contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat_frontends/jp/cmd_file.py:306 +msgid "jids accepted (accept everything if none is specified)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:312 +#, fuzzy +msgid "accept multiple files (you'll have to stop manually)" +msgstr "Accepte plusieurs fichiers (vous devrez arrêter le programme à la main)" + +#: sat_frontends/jp/cmd_file.py:318 +#, fuzzy +msgid "force overwritting of existing files (/!\\ name is choosed by sender)" +msgstr "Force le remplacement des fichiers existants" + +#: sat_frontends/jp/cmd_file.py:326 +msgid "destination path (default: working directory)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:337 +msgid "hash checked: {metadata['hash_algo']}:{metadata['hash']}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:340 +msgid "hash is checked but hash value is missing" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:342 +msgid "hash can't be verified" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:345 +#, fuzzy +msgid "Error while receiving file: {e}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_file.py:354 sat_frontends/jp/cmd_pipe.py:111 +msgid "Action has no XMLUI" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:359 sat_frontends/jp/cmd_pipe.py:115 +msgid "Invalid XMLUI received" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:369 sat_frontends/jp/cmd_pipe.py:126 +msgid "Ignoring action without from_jid data" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:374 sat_frontends/jp/cmd_file.py:395 +msgid "ignoring action without progress id" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:379 +msgid "File refused because overwrite is needed" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:397 +msgid "Overwriting needed" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:401 +#, fuzzy +msgid "Overwrite accepted" +msgstr "accepté" + +#: sat_frontends/jp/cmd_file.py:403 +msgid "Refused to overwrite" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:417 +msgid "invalid \"from_jid\" value received, ignoring: {value}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:424 +msgid "ignoring action without \"from_jid\" value" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:426 +msgid "Confirmation needed for request from an entity not in roster" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:431 +msgid "Sender confirmed because she or he is explicitly expected" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:439 +msgid "Session refused for {from_jid}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:446 +msgid "Given path is not a directory !" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:450 +msgid "waiting for incoming file request" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:461 +msgid "download a file from URI" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:470 +msgid "destination file (DEFAULT: filename from URL)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:478 +msgid "URI of the file to retrieve" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:481 +msgid "File download started" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:484 +msgid "File downloaded successfully" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:487 +#, fuzzy, python-format +msgid "Error while downloading file: {}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_file.py:513 +msgid "file download cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:534 +msgid "upload a file" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:542 +msgid "encrypt file using AES-GCM" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:544 +msgid "file to upload" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:548 +msgid "jid of upload component (nothing to autodetect)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:553 +msgid "ignore invalide TLS certificate (/!\\ Dangerous /!\\)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:557 +msgid "File upload started" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:560 +msgid "File uploaded successfully" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:566 +msgid "URL to retrieve the file:" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:571 +msgid "Error while uploading file: {}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:593 +#, fuzzy, python-format +msgid "file {file_} doesn't exist !" +msgstr "Le fichier [%s] n'existe pas !" + +#: sat_frontends/jp/cmd_file.py:597 +msgid "{file_} is a dir! Can't upload a dir" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:633 +msgid "set affiliations for a shared file/directory" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:641 sat_frontends/jp/cmd_file.py:695 +#: sat_frontends/jp/cmd_file.py:747 sat_frontends/jp/cmd_file.py:801 +#: sat_frontends/jp/cmd_file.py:1002 +msgid "namespace of the repository" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:647 sat_frontends/jp/cmd_file.py:701 +#: sat_frontends/jp/cmd_file.py:753 sat_frontends/jp/cmd_file.py:807 +#: sat_frontends/jp/cmd_file.py:1007 +msgid "path to the repository" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:657 sat_frontends/jp/cmd_pubsub.py:453 +msgid "entity/affiliation couple(s)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:661 sat_frontends/jp/cmd_file.py:767 +msgid "jid of file sharing entity" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:687 +msgid "retrieve affiliations of a shared file/directory" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:705 sat_frontends/jp/cmd_file.py:811 +msgid "jid of sharing entity" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:729 +msgid "affiliations management" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:739 +msgid "set configuration for a shared file/directory" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:763 sat_frontends/jp/cmd_pubsub.py:282 +msgid "configuration field to set (required)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:793 +msgid "retrieve configuration of a shared file/directory" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:838 +#, fuzzy +msgid "file sharing node configuration" +msgstr "Demande de confirmation pour un transfer de fichier demandée" + +#: sat_frontends/jp/cmd_file.py:850 +msgid "retrieve files shared by an entity" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:859 +msgid "path to the directory containing the files" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:865 +msgid "jid of sharing entity (nothing to check our own jid)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:874 +msgid "unknown file type: {type}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:923 +msgid "share a file or directory" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:931 +msgid "virtual name to use (default: use directory/file name)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:941 +msgid "jid of contacts allowed to retrieve the files" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:946 +msgid "share publicly the file(s) (/!\\ *everybody* will be able to access them)" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:953 +msgid "path to a file or directory to share" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:978 +msgid "{path} shared under the name \"{name}\"" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:988 +msgid "send invitation for a shared repository" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:996 +#, fuzzy +msgid "name of the repository" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_file.py:1014 +msgid "type of the repository" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1019 +msgid "https URL of a image to use as thumbnail" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1023 +msgid "jid of the file sharing service hosting the repository" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1027 +#, fuzzy +msgid "jid of the person to invite" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_file.py:1035 +msgid "only http(s) links are allowed with --thumbnail" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1053 +msgid "invitation sent to {jid}" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1068 +msgid "files sharing management" +msgstr "" + +#: sat_frontends/jp/cmd_file.py:1077 +msgid "files sending/receiving/management" +msgstr "" + +#: sat_frontends/jp/cmd_forums.py:45 +msgid "edit forums" +msgstr "" + +#: sat_frontends/jp/cmd_forums.py:54 sat_frontends/jp/cmd_forums.py:123 +msgid "forum key (DEFAULT: default forums)" +msgstr "" + +#: sat_frontends/jp/cmd_forums.py:74 +msgid "forums have been edited" +msgstr "" + +#: sat_frontends/jp/cmd_forums.py:115 +msgid "get forums structure" +msgstr "" + +#: sat_frontends/jp/cmd_forums.py:168 sat_frontends/jp/cmd_pubsub.py:733 +#, fuzzy +msgid "no schema found" +msgstr "Aucun transport trouvé" + +#: sat_frontends/jp/cmd_forums.py:180 +msgid "Forums structure edition" +msgstr "" + +#: sat_frontends/jp/cmd_identity.py:37 +msgid "get identity data" +msgstr "" + +#: sat_frontends/jp/cmd_identity.py:45 +msgid "entity to check" +msgstr "" + +#: sat_frontends/jp/cmd_identity.py:68 +msgid "update identity data" +msgstr "" + +#: sat_frontends/jp/cmd_identity.py:77 +msgid "nicknames of the entity" +msgstr "" + +#: sat_frontends/jp/cmd_identity.py:101 +msgid "identity management" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:38 +msgid "service discovery" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:42 +msgid "entity to discover" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:49 +msgid "type of data to discover" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:51 +msgid "node to use" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:57 +#, fuzzy +msgid "ignore cache" +msgstr "fichier [%s] déjà en cache" + +#: sat_frontends/jp/cmd_info.py:69 +msgid "category" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:111 +msgid "node" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:116 +msgid "Features" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:118 +#, fuzzy +msgid "Identities" +msgstr "Petite" + +#: sat_frontends/jp/cmd_info.py:120 +msgid "Extensions" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:122 +msgid "Items" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:151 sat_frontends/jp/cmd_info.py:166 +msgid "error while doing discovery: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:190 +msgid "software version" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:193 sat_frontends/jp/cmd_info.py:258 +#, fuzzy +msgid "Entity to request" +msgstr "Demande de suppression de contact" + +#: sat_frontends/jp/cmd_info.py:201 +#, fuzzy, python-format +msgid "error while trying to get version: {e}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_info.py:207 +msgid "Software name: {name}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:209 +msgid "Software version: {version}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:211 +msgid "Operating System: {os}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:225 +#, fuzzy +msgid "running session" +msgstr "Lancement de l'application" + +#: sat_frontends/jp/cmd_info.py:243 +msgid "Error getting session infos: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:253 +msgid "devices of an entity" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:267 +msgid "Error getting devices infos: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_info.py:283 +msgid "Get various pieces of information on entities" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:61 +msgid "encoding of the input data" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:69 +msgid "standard input" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:77 +msgid "short option" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:85 +msgid "long option" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:93 +msgid "positional argument" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:101 +msgid "ignore value" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:107 +msgid "don't actually run commands but echo what would be launched" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:110 +msgid "log stdout to FILE" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:113 +msgid "log stderr to FILE" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:128 sat_frontends/jp/cmd_input.py:193 +msgid "arguments in input data and in arguments sequence don't match" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:155 sat_frontends/jp/cmd_input.py:207 +msgid "values: " +msgstr "" + +#: sat_frontends/jp/cmd_input.py:161 +msgid "**SKIPPING**\n" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:184 +msgid "Invalid argument, an option type is expected, got {type_}:{name}" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:199 +msgid "command {idx}" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:252 sat_frontends/primitivus/xmlui.py:461 +msgid "OK" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:254 +msgid "FAILED" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:274 +msgid "comma-separated values" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:283 +msgid "starting row (previous ones will be ignored)" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:291 +msgid "split value in several options" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:299 +msgid "action to do on empty value ({choices})" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:314 +msgid "--empty value must be one of {choices}" +msgstr "" + +#: sat_frontends/jp/cmd_input.py:349 +msgid "launch command with external input" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:38 +msgid "create and send an invitation" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:127 +msgid "you need to specify an email address to send email invitation" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:161 +#, fuzzy +msgid "get invitation data" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_invitation.py:165 +#: sat_frontends/jp/cmd_invitation.py:225 +#: sat_frontends/jp/cmd_invitation.py:289 +#, fuzzy +msgid "invitation UUID" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_invitation.py:170 +msgid "start profile session and retrieve jid" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:185 +msgid "can't get invitation data: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:198 +#, fuzzy +msgid "can't start session: {e}" +msgstr "Construction du jeu de Tarot" + +#: sat_frontends/jp/cmd_invitation.py:208 +msgid "can't retrieve jid: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:221 +#, fuzzy +msgid "delete guest account" +msgstr "Enregistrement d'un nouveau compte" + +#: sat_frontends/jp/cmd_invitation.py:233 +msgid "can't delete guest account: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:242 +msgid "modify existing invitation" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:299 +msgid "you can't set {arg_name} in both optional argument and extra" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:314 +msgid "invitations have been modified successfuly" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:328 +#, fuzzy +msgid "list invitations data" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_invitation.py:346 +msgid "return only invitations linked to this profile" +msgstr "" + +#: sat_frontends/jp/cmd_invitation.py:370 +msgid "invitation of user(s) without XMPP account" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:43 sat_frontends/jp/cmd_list.py:81 +#: sat_frontends/jp/cmd_list.py:150 sat_frontends/jp/cmd_merge_request.py:39 +#: sat_frontends/jp/cmd_merge_request.py:124 +#: sat_frontends/jp/cmd_merge_request.py:169 +msgid "auto" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:45 +msgid "get lists" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:82 +msgid "set a list item" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:94 +msgid "field(s) to set (required)" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:101 +msgid "update existing item instead of replacing it (DEFAULT: auto)" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:107 +msgid "id, URL of the item to update, or nothing for new item" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:151 +msgid "delete a list item" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:156 sat_frontends/jp/cmd_pubsub.py:884 +#: sat_frontends/jp/cmd_roster.py:135 +msgid "delete without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:159 sat_frontends/jp/cmd_pubsub.py:887 +#, fuzzy +msgid "notify deletion" +msgstr "Sélection du contrat" + +#: sat_frontends/jp/cmd_list.py:163 +msgid "id of the item to delete" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:169 +msgid "You need to specify a list item to delete" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:171 +#, fuzzy, python-format +msgid "Are you sure to delete list item {item_id} ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/jp/cmd_list.py:174 sat_frontends/jp/cmd_pubsub.py:897 +msgid "item deletion cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:184 sat_frontends/jp/cmd_pubsub.py:907 +#, fuzzy, python-format +msgid "can't delete item: {e}" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/jp/cmd_list.py:187 sat_frontends/jp/cmd_pubsub.py:910 +msgid "item {item} has been deleted" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:200 +msgid "import tickets from external software/dataset" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:225 +msgid "" +"specified field in import data will be put in dest field (default: use " +"same field name, or ignore if it doesn't exist)" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:235 +msgid "PubSub service where the items must be uploaded (default: server)" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:242 +msgid "PubSub node where the items must be uploaded (default: tickets' defaults)" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:257 +msgid "Tickets upload started" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:260 +msgid "Tickets uploaded successfully" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:264 +#, fuzzy, python-format +msgid "Error while uploading tickets: {error_msg}" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/jp/cmd_list.py:319 +msgid "" +"fields_map must be specified either preencoded in --option or using " +"--map, but not both at the same time" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:337 +msgid "Error while trying to import tickets: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_list.py:350 +msgid "pubsub lists handling" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:40 +msgid "publish or update a merge request" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:48 +msgid "id or URL of the request to update, or nothing for a new one" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:55 +#: sat_frontends/jp/cmd_merge_request.py:179 +msgid "path of the repository (DEFAULT: current directory)" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:61 +msgid "publish merge request without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:68 +msgid "labels to categorize your request" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:75 +msgid "" +"You are going to publish your changes to service [{service}], are you " +"sure ?" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:80 +msgid "merge request publication cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:105 +msgid "Merge request published at {published_id}" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:110 +msgid "Merge request published" +msgstr "" + +#: sat_frontends/jp/cmd_merge_request.py:125 +#, fuzzy +msgid "get a merge request" +msgstr "Demande de changement de statut" + +#: sat_frontends/jp/cmd_merge_request.py:170 +#, fuzzy +msgid "import a merge request" +msgstr "Demande de changement de statut" + +#: sat_frontends/jp/cmd_merge_request.py:209 +msgid "merge-request management" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:34 +#, fuzzy +msgid "send a message to a contact" +msgstr "Attend qu'un fichier soit envoyé par un contact" + +#: sat_frontends/jp/cmd_message.py:38 +msgid "language of the message" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:44 +#, fuzzy +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." + +#: sat_frontends/jp/cmd_message.py:53 +#, fuzzy +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 ;))" + +#: sat_frontends/jp/cmd_message.py:60 +msgid "subject of the message" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:63 +msgid "language of subject" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:70 +msgid "type of the message" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:73 +msgid "encrypt message using given algorithm" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:79 +msgid "XHTML body" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:80 +msgid "rich body" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:195 +msgid "query archives using MAM" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:203 +msgid "start fetching archive from this date (default: from the beginning)" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:207 +msgid "end fetching archive after this date (default: no limit)" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:210 +msgid "retrieve only archives with this jid" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:213 +msgid "maximum number of items to retrieve, using RSM (default: 20))" +msgstr "" + +#: sat_frontends/jp/cmd_message.py:276 +msgid "messages handling" +msgstr "" + +#: sat_frontends/jp/cmd_param.py:32 +#, fuzzy +msgid "get a parameter value" +msgstr "Impossible de charger les paramètres !" + +#: sat_frontends/jp/cmd_param.py:37 sat_frontends/jp/cmd_param.py:94 +#, fuzzy +msgid "category of the parameter" +msgstr "Impossible de charger les paramètres !" + +#: sat_frontends/jp/cmd_param.py:39 sat_frontends/jp/cmd_param.py:95 +#: sat_frontends/jp/cmd_param.py:96 +#, fuzzy +msgid "name of the parameter" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_param.py:45 +msgid "name of the attribute to get" +msgstr "" + +#: sat_frontends/jp/cmd_param.py:48 sat_frontends/jp/cmd_param.py:98 +msgid "security limit" +msgstr "" + +#: sat_frontends/jp/cmd_param.py:62 +#, fuzzy +msgid "can't find requested parameters: {e}" +msgstr "Impossible de charger les paramètres !" + +#: sat_frontends/jp/cmd_param.py:79 +#, fuzzy +msgid "can't find requested parameter: {e}" +msgstr "Impossible de charger les paramètres !" + +#: sat_frontends/jp/cmd_param.py:90 +msgid "set a parameter value" +msgstr "" + +#: sat_frontends/jp/cmd_param.py:111 +#, fuzzy +msgid "can't set requested parameter: {e}" +msgstr "Mauvais nom de profile" + +#: sat_frontends/jp/cmd_param.py:125 +#, fuzzy, python-format +msgid "save parameters template to xml file" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat_frontends/jp/cmd_param.py:129 +msgid "output file" +msgstr "" + +#: sat_frontends/jp/cmd_param.py:136 +#, fuzzy, python-format +msgid "can't save parameters to file: {e}" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat_frontends/jp/cmd_param.py:140 +#, fuzzy, python-format +msgid "parameters saved to file {filename}" +msgstr "Échec de la désinscription: %s" + +#: sat_frontends/jp/cmd_param.py:155 +#, fuzzy, python-format +msgid "load parameters template from xml file" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat_frontends/jp/cmd_param.py:159 +#, fuzzy +msgid "input file" +msgstr "Envoi un fichier" + +#: sat_frontends/jp/cmd_param.py:166 +#, fuzzy, python-format +msgid "can't load parameters from file: {e}" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat_frontends/jp/cmd_param.py:170 +#, fuzzy, python-format +msgid "parameters loaded from file {filename}" +msgstr "Échec de la désinscription: %s" + +#: sat_frontends/jp/cmd_param.py:182 +#, fuzzy +msgid "Save/load parameters template" +msgstr "Impossible de charger le modèle des paramètres !" + +#: sat_frontends/jp/cmd_ping.py:29 +msgid "ping XMPP entity" +msgstr "" + +#: sat_frontends/jp/cmd_ping.py:32 +msgid "jid to ping" +msgstr "" + +#: sat_frontends/jp/cmd_ping.py:34 +msgid "output delay only (in s)" +msgstr "" + +#: sat_frontends/jp/cmd_ping.py:41 +msgid "can't do the ping: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pipe.py:38 +msgid "send a pipe a stream" +msgstr "" + +#: sat_frontends/jp/cmd_pipe.py:97 +#, fuzzy +msgid "receive a pipe stream" +msgstr "Lancement du flux" + +#: sat_frontends/jp/cmd_pipe.py:104 +msgid "Jids accepted (none means \"accept everything\")" +msgstr "" + +#: sat_frontends/jp/cmd_pipe.py:159 +msgid "stream piping through XMPP" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:33 +#, fuzzy +msgid "The name of the profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_profile.py:51 +msgid "You need to use either --connect or --start-session" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:78 +#, fuzzy +msgid "the name of the profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_profile.py:81 +msgid "the password of the profile" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:83 sat_frontends/jp/cmd_profile.py:238 +#, fuzzy +msgid "the jid of the profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_profile.py:86 +msgid "the password of the XMPP account (use profile password if not specified)" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:93 sat_frontends/jp/cmd_profile.py:247 +msgid "connect this profile automatically when backend starts" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:97 +msgid "set to component import name (entry point) if this is a component" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:154 +msgid "delete profile without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:174 +#, fuzzy +msgid "get information about a profile" +msgstr "Demande de contacts pour un profile inexistant" + +#: sat_frontends/jp/cmd_profile.py:180 +msgid "show the XMPP password IN CLEAR TEXT" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:184 +#, fuzzy +msgid "XMPP password" +msgstr "Mot de passe:" + +#: sat_frontends/jp/cmd_profile.py:185 +msgid "autoconnect (backend)" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:209 +msgid "get clients profiles only" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:229 +msgid "modify an existing profile" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:234 +#, fuzzy +msgid "change the password of the profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_profile.py:237 +msgid "disable profile password (dangerous!)" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:240 +msgid "change the password of the XMPP account" +msgstr "" + +#: sat_frontends/jp/cmd_profile.py:243 +#, fuzzy +msgid "set as default profile" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/jp/cmd_profile.py:280 +#, fuzzy +msgid "profile commands" +msgstr "Mauvais nom de profile" + +#: sat_frontends/jp/cmd_pubsub.py:59 +#, fuzzy +msgid "retrieve node configuration" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_pubsub.py:68 +msgid "data key to filter" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:116 +#, fuzzy +msgid "create a node" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/jp/cmd_pubsub.py:135 sat_frontends/jp/cmd_pubsub.py:288 +msgid "don't prepend \"pubsub#\" prefix to field names" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:158 +msgid "can't create node: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:162 +#, fuzzy +msgid "node created successfully: " +msgstr "Inscription réussie" + +#: sat_frontends/jp/cmd_pubsub.py:176 +msgid "purge a node (i.e. remove all items from it)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:184 +#, fuzzy +msgid "purge node without confirmation" +msgstr "désinscription confirmée pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:190 +msgid "" +"Are you sure to purge PEP node [{node}]? This will delete ALL items from " +"it!" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:195 +msgid "" +"Are you sure to delete node [{node}] on service [{service}]? This will " +"delete ALL items from it!" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:199 +msgid "node purge cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:208 +msgid "can't purge node: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:211 +msgid "node [{node}] purged successfully" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:223 +#, fuzzy +msgid "delete a node" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_pubsub.py:231 +msgid "delete node without confirmation" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:237 +#, fuzzy, python-format +msgid "Are you sure to delete PEP node [{node}] ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/jp/cmd_pubsub.py:241 +#, fuzzy, python-format +msgid "Are you sure to delete node [{node}] on service [{service}]?" +msgstr "" +"Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur" +" %(server)s ?" + +#: sat_frontends/jp/cmd_pubsub.py:244 +msgid "node deletion cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:256 +msgid "node [{node}] deleted successfully" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:270 +#, fuzzy +msgid "set node configuration" +msgstr "Connexion..." + +#: sat_frontends/jp/cmd_pubsub.py:309 +#, fuzzy +msgid "node configuration successful" +msgstr "Inscription réussie" + +#: sat_frontends/jp/cmd_pubsub.py:320 +msgid "import raw XML to a node" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:327 sat_frontends/jp/cmd_pubsub.py:1608 +msgid "do a pubsub admin request, needed to change publisher" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:332 +msgid "" +"path to the XML file with data to import. The file must contain whole XML" +" of each item to import." +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:359 +msgid "You are not using list of pubsub items, we can't import this file" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:370 +msgid "Items are imported without using admin mode, publisher can't be changed" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:391 +msgid "items published with id(s) {items_ids}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:396 sat_frontends/jp/cmd_pubsub.py:1641 +msgid "items published" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:409 +msgid "retrieve node affiliations (for node owner)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:439 +msgid "set affiliations (for node owner)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:469 +msgid "affiliations have been set" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:481 +msgid "set or retrieve node affiliations" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:494 +msgid "retrieve node subscriptions (for node owner)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:532 +#, fuzzy, python-format +msgid "subscription must be one of {}" +msgstr "inscription approuvée pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:548 +msgid "set/modify subscriptions (for node owner)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:563 +msgid "entity/subscription couple(s)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:578 +#, fuzzy, python-format +msgid "subscriptions have been set" +msgstr "inscription approuvée pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:590 +msgid "get or modify node subscriptions" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:603 +msgid "set/replace a schema" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:607 +msgid "schema to set (must be XML)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:621 sat_frontends/jp/cmd_pubsub.py:656 +msgid "schema has been set" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:637 +msgid "edit a schema" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:709 +msgid "get schema" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:742 +msgid "data schema manipulation" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:761 +msgid "node handling" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:774 +msgid "publish a new item or update an existing one" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:783 +msgid "id, URL of the item to update, keyword, or nothing for new item" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:805 +#, fuzzy, python-format +msgid "can't send item: {e}" +msgstr "message reçu de: %s" + +#: sat_frontends/jp/cmd_pubsub.py:827 +msgid "get pubsub item(s)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:835 +#, fuzzy +msgid "subscription id" +msgstr "demande d'inscription pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:879 +#, fuzzy +msgid "delete an item" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_pubsub.py:892 +msgid "You need to specify an item to delete" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:894 +#, fuzzy, python-format +msgid "Are you sure to delete item {item_id} ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/jp/cmd_pubsub.py:924 +msgid "edit an existing or new pubsub item" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:968 +msgid "Item has not payload" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:992 +msgid "rename a pubsub item" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1024 +msgid "subscribe to a node" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1039 +msgid "can't subscribe to node: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1042 +#, fuzzy +msgid "subscription done" +msgstr "demande d'inscription pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:1044 +msgid "subscription id: {sub_id}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1059 +msgid "unsubscribe from a node" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1073 +msgid "can't unsubscribe from node: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1076 +#, fuzzy, python-format +msgid "subscription removed" +msgstr "inscription approuvée pour [%s]" + +#: sat_frontends/jp/cmd_pubsub.py:1088 +msgid "retrieve all subscriptions on a service" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1102 +msgid "can't retrieve subscriptions: {e}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1117 +msgid "retrieve all affiliations on a service" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1160 +msgid "search items corresponding to filters" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1185 +msgid "maximum depth of recursion (will search linked nodes if > 0, DEFAULT: 0)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1197 +msgid "maximum number of items to get per node ({} to get all items, DEFAULT: 30)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1207 +msgid "namespace to use for xpath" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1216 +msgid "filters" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1217 +msgid "only items corresponding to following filters will be kept" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1226 +msgid "full text filter, item must contain this string (XML included)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1235 +msgid "like --text but using a regular expression" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1244 +msgid "filter items which has elements matching this xpath" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1253 +msgid "" +"Python expression which much return a bool (True to keep item, False to " +"reject it). \"item\" is raw text item, \"item_xml\" is lxml's " +"etree.Element" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1266 +msgid "filters flags" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1267 +msgid "filters modifiers (change behaviour of following filters)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1278 +msgid "(don't) ignore case in following filters (DEFAULT: case sensitive)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1289 +msgid "(don't) invert effect of following filters (DEFAULT: don't invert)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1300 +msgid "(don't) use DOTALL option for regex (DEFAULT: don't use)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1311 +msgid "keep only the matching part of the item" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1320 +msgid "action to do on found items (DEFAULT: print)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1383 +msgid "" +"item doesn't looks like XML, you have probably used --only-matching " +"somewhere before and we have no more XML" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1420 +msgid "--only-matching used with fixed --text string, are you sure?" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1441 +msgid "can't use xpath: {reason}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1476 +#, fuzzy, python-format +msgid "unknown filter type {type}" +msgstr "Type d'action inconnu" + +#: sat_frontends/jp/cmd_pubsub.py:1534 +msgid "executed command failed with exit code {ret}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1566 +msgid "Command can only be used with {actions} actions" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1572 +msgid "you need to specify a command to execute" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1575 +msgid "empty node is not handled yet" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1596 +msgid "modify items of a node using an external command/script" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1603 +msgid "apply transformation (DEFAULT: do a dry run)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1614 +msgid "if command return a non zero exit code, ignore the item and continue" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1622 +msgid "get all items by looping over all pages using RSM" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1626 +msgid "" +"path to the command to use. Will be called repetitivly with an item as " +"input. Output (full item XML) will be used as new one. Return \"DELETE\" " +"string to delete the item, and \"SKIP\" to ignore it" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1636 +msgid "items published with ids {item_ids}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1659 +msgid "Can't retrieve all items, RSM metadata not available" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1664 +msgid "Can't retrieve all items, bad RSM metadata: {msg}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1670 +msgid "All items transformed" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1674 +msgid "Retrieving next page ({page_idx}/{page_total})" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1712 +msgid "Duplicate found on item {item_id}, we have probably handled all items." +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1749 +msgid "Deleting item {item_id}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1766 +msgid "Skipping item {item_id}" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1860 sat_frontends/jp/cmd_uri.py:53 +msgid "build URI" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1868 +msgid "profile (used when no server is specified)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1908 +msgid "create a Pubsub hook" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1917 +msgid "hook type" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1923 +msgid "make hook persistent across restarts" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1927 +msgid "argument of the hook (depend of the type)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1936 +msgid "{path} is not a file" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1965 +msgid "delete a Pubsub hook" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1974 +msgid "hook type to remove, empty to remove all (DEFAULT: remove all)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:1981 +msgid "argument of the hook to remove, empty to remove all (DEFAULT: remove all)" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:2001 +msgid "{nb_deleted} hook(s) have been deleted" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:2013 +#, fuzzy +msgid "list hooks of a profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_pubsub.py:2029 +#, fuzzy +msgid "No hook found." +msgstr "Aucune donnée trouvée" + +#: sat_frontends/jp/cmd_pubsub.py:2043 +msgid "trigger action on Pubsub notifications" +msgstr "" + +#: sat_frontends/jp/cmd_pubsub.py:2067 +msgid "PubSub nodes/items management" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:36 +msgid "retrieve the roster entities" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:89 +msgid "set metadata for a roster entity" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:93 +msgid "name to use for this entity" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:96 +msgid "groups for this entity" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:99 +msgid "replace all metadata instead of adding them" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:101 sat_frontends/jp/cmd_roster.py:138 +#, fuzzy +msgid "jid of the roster entity" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/jp/cmd_roster.py:131 +#, fuzzy, python-format +msgid "remove an entity from roster" +msgstr "supppression du contact %s" + +#: sat_frontends/jp/cmd_roster.py:142 +#, fuzzy, python-format +msgid "Are you sure to delete {entity} fril your roster?" +msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" + +#: sat_frontends/jp/cmd_roster.py:145 +msgid "entity deletion cancelled" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:158 +msgid "Show statistics about a roster" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:226 +msgid "purge the roster from its contacts with no subscription" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:231 +#, fuzzy, python-format +msgid "also purge contacts with no 'from' subscription" +msgstr "Le contact %s a refusé votre inscription" + +#: sat_frontends/jp/cmd_roster.py:234 +#, fuzzy, python-format +msgid "also purge contacts with no 'to' subscription" +msgstr "Le contact %s a refusé votre inscription" + +#: sat_frontends/jp/cmd_roster.py:306 +msgid "do a full resynchronisation of roster with server" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:318 +msgid "Roster resynchronized" +msgstr "" + +#: sat_frontends/jp/cmd_roster.py:327 +msgid "Manage an entity's roster" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:33 +msgid "" +"Welcome to {app_name} shell, the Salut à Toi shell !\n" +"\n" +"This enrironment helps you using several {app_name} commands with similar" +" parameters.\n" +"\n" +"To quit, just enter \"quit\" or press C-d.\n" +"Enter \"help\" or \"?\" to know what to do\n" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:48 +msgid "launch jp in shell (REPL) mode" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:63 +msgid "bad command path" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:104 +msgid "COMMAND {external}=> {args}" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:105 +msgid "(external) " +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:149 +#, fuzzy +msgid "Shell commands:" +msgstr "Mauvais nom de profile" + +#: sat_frontends/jp/cmd_shell.py:152 +#, fuzzy +msgid "Action commands:" +msgstr "Mauvais nom de profile" + +#: sat_frontends/jp/cmd_shell.py:172 +msgid "verbose mode is {status}" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:173 +msgid "ENABLED" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:173 +msgid "DISABLED" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:214 +msgid "arg profile={profile} (logged profile)" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:236 +msgid "no argument in USE" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:238 +msgid "arguments in USE:" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:274 +msgid "argument {name} not found" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:280 +msgid "argument {name} removed" +msgstr "" + +#: sat_frontends/jp/cmd_shell.py:288 +msgid "good bye!" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:37 +msgid "parse URI" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:42 +msgid "XMPP URI to parse" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:57 +msgid "URI type" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:58 +msgid "URI path" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:66 +msgid "URI fields" +msgstr "" + +#: sat_frontends/jp/cmd_uri.py:80 +msgid "XMPP URI parsing/generation" +msgstr "" + +#: sat_frontends/jp/common.py:437 +msgid "no item found at all, we create a new one" +msgstr "" + +#: sat_frontends/jp/common.py:440 +msgid "item \"{item}\" not found, we create a new item withthis id" +msgstr "" + +#: sat_frontends/jp/common.py:458 +msgid "item \"{item}\" found, we edit it" +msgstr "" + +#: sat_frontends/jp/common.py:785 +msgid "No {key} URI specified for this project, please specify service and node" +msgstr "" + +#: sat_frontends/jp/common.py:821 +msgid "Invalid URI found: {uri}" +msgstr "" + +#: sat_frontends/jp/loops.py:28 +msgid "User interruption: good bye" +msgstr "Interrompu par l'utilisateur: au revoir" + +#: sat_frontends/jp/output_template.py:53 +msgid "Can't find requested template: {template_path}" +msgstr "" + +#: sat_frontends/jp/output_template.py:74 +msgid "" +"no default template set for this command, you need to specify a template " +"using --oo template=[path/to/template.html]" +msgstr "" + +#: sat_frontends/jp/output_template.py:89 +msgid "Can't parse template, please check its syntax" +msgstr "" + +#: sat_frontends/jp/output_template.py:109 +msgid "" +"Browser opening requested.\n" +"Temporary files are put in the following directory, you'll have to delete" +" it yourself once finished viewing: {}" +msgstr "" + +#: sat_frontends/jp/output_xml.py:56 +msgid "" +"Pygments is not available, syntax highlighting is not possible. Please " +"install if from http://pygments.org or with pip install pygments" +msgstr "" + +#: sat_frontends/jp/xml_tools.py:50 +msgid "Can't parse the payload XML in input: {msg}" +msgstr "" + +#: sat_frontends/jp/xml_tools.py:62 +msgid " can only have one child element (the payload)" +msgstr "" + +#: sat_frontends/jp/xmlui_manager.py:224 +msgid "(enter: {value})" +msgstr "" + +#: sat_frontends/jp/xmlui_manager.py:318 +msgid "your choice (0-{limit_max}): " +msgstr "" + +#: sat_frontends/jp/xmlui_manager.py:348 +msgid "your choice (0,1): " +msgstr "" + +#: sat_frontends/primitivus/base.py:90 +#, fuzzy, python-format +msgid "Error while sending message ({})" +msgstr "Erreur en tentant de rejoindre le salon" + +#: sat_frontends/primitivus/base.py:135 +msgid "Please specify the globbing pattern to search for" +msgstr "" + +#: sat_frontends/primitivus/base.py:377 +#, fuzzy +msgid "Configuration Error" +msgstr "Connexion..." + +#: sat_frontends/primitivus/base.py:377 +msgid "" +"Something went wrong while reading the configuration, please check " +":messages" +msgstr "" + +#: sat_frontends/primitivus/base.py:504 +msgid "Pleeeeasse, I can't even breathe !" +msgstr "Pitiééééééééé, je ne peux même pas respirer !" + +#: sat_frontends/primitivus/base.py:534 +#: sat_frontends/primitivus/profile_manager.py:64 +#, fuzzy +msgid "Connect" +msgstr "Connexion..." + +#: sat_frontends/primitivus/base.py:536 +#, fuzzy +msgid "Parameters" +msgstr "&Paramètres" + +#: sat_frontends/primitivus/base.py:537 sat_frontends/primitivus/base.py:851 +msgid "About" +msgstr "À propos" + +#: sat_frontends/primitivus/base.py:538 +#, fuzzy +msgid "Exit" +msgstr "Quitter" + +#: sat_frontends/primitivus/base.py:542 +msgid "Join room" +msgstr "Rejoindre un salon" + +#: sat_frontends/primitivus/base.py:547 +#, fuzzy +msgid "Main menu" +msgstr "Construction des menus" + +#: sat_frontends/primitivus/base.py:658 +msgid "{app}: a new event has just happened{entity}" +msgstr "" + +#: sat_frontends/primitivus/base.py:736 +#, fuzzy +msgid "Chat menu" +msgstr "Construction des menus" + +#: sat_frontends/primitivus/base.py:790 +#, fuzzy +msgid "Unmanaged action" +msgstr "Tab inconnu" + +#: sat_frontends/primitivus/base.py:801 +#, fuzzy +msgid "unkown" +msgstr "Messagerie inconnue" + +#: sat_frontends/primitivus/base.py:831 +#, fuzzy, python-format +msgid "Can't get parameters (%s)" +msgstr "Impossible de charger les paramètres !" + +#: sat_frontends/primitivus/base.py:846 +msgid "Entering a MUC room" +msgstr "Entrée dans le salon MUC" + +#: sat_frontends/primitivus/base.py:846 +#, fuzzy +msgid "Please enter MUC's JID" +msgstr "Veuillez entrer le JID de votre nouveau contact" + +#: sat_frontends/primitivus/chat.py:40 +msgid "{} occupants" +msgstr "" + +#: sat_frontends/primitivus/chat.py:381 +msgid "Game" +msgstr "Jeu" + +#: sat_frontends/primitivus/chat.py:502 +msgid "You have been mentioned by {nick} in {room}" +msgstr "" + +#: sat_frontends/primitivus/chat.py:513 +msgid "{entity} is talking to you" +msgstr "" + +#: sat_frontends/primitivus/chat.py:612 +msgid "Results for searching the globbing pattern: {}" +msgstr "" + +#: sat_frontends/primitivus/chat.py:618 +msgid "Type ':history ' to reset the chat history" +msgstr "" + +#: sat_frontends/primitivus/chat.py:652 +#, python-format +msgid "Primitivus: %s is talking to you" +msgstr "" + +#: sat_frontends/primitivus/chat.py:656 +#, fuzzy, python-format +msgid "Primitivus: %(user)s mentioned you in room '%(room)s'" +msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" + +#: sat_frontends/primitivus/chat.py:666 +#, fuzzy +msgid "Can't start game" +msgstr "Construction du jeu de Tarot" + +#: sat_frontends/primitivus/chat.py:667 +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" + +#: sat_frontends/primitivus/chat.py:698 +msgid "Change title" +msgstr "" + +#: sat_frontends/primitivus/chat.py:699 +#, fuzzy +msgid "Enter the new title" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/primitivus/game_tarot.py:290 +msgid "Please choose your contrat" +msgstr "Veuillez choisir votre contrat" + +#: sat_frontends/primitivus/game_tarot.py:311 +msgid "You win \\o/" +msgstr "Victoire \\o/" + +#: sat_frontends/primitivus/game_tarot.py:311 +msgid "You loose :(" +msgstr "Vous perdez :(" + +#: sat_frontends/primitivus/game_tarot.py:331 +msgid "Cards played are invalid !" +msgstr "Les cartes jouées sont invalides !" + +#: sat_frontends/primitivus/game_tarot.py:369 +msgid "Do you put these cards in chien ?" +msgstr "Voulez-vous placer ces cartes au chien ?" + +#: sat_frontends/primitivus/profile_manager.py:36 +#, fuzzy +msgid "Login:" +msgstr "Identifiant" + +#: sat_frontends/primitivus/profile_manager.py:37 +msgid "Password:" +msgstr "Mot de passe:" + +#: sat_frontends/primitivus/profile_manager.py:48 +msgid "New" +msgstr "Nouveau" + +#: sat_frontends/primitivus/profile_manager.py:49 +msgid "Delete" +msgstr "Suppression" + +#: sat_frontends/primitivus/profile_manager.py:81 +#, fuzzy +msgid "Profile Manager" +msgstr "Mauvais nom de profile" + +#: sat_frontends/primitivus/profile_manager.py:142 +msgid "Can't create profile" +msgstr "" + +#: sat_frontends/primitivus/profile_manager.py:150 +#, fuzzy +msgid "New profile" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/primitivus/profile_manager.py:151 +#, fuzzy +msgid "Please enter a new profile name" +msgstr "Veuillez entrer le nom du nouveau profile" + +#: sat_frontends/primitivus/profile_manager.py:160 +#, fuzzy, python-format +msgid "Are you sure you want to delete the profile {} ?" +msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" + +#: sat_frontends/primitivus/progress.py:37 +msgid "Clear progress list" +msgstr "Effacer la liste" + +#: sat_frontends/primitivus/status.py:57 +msgid "Set your presence" +msgstr "" + +#: sat_frontends/primitivus/status.py:67 +msgid "Set your status" +msgstr "" + +#: sat_frontends/primitivus/status.py:68 +msgid "New status" +msgstr "" + +#: sat_frontends/primitivus/xmlui.py:78 +#, fuzzy +msgid "Unknown div_char" +msgstr "Type d'action inconnu" + +#: sat_frontends/primitivus/xmlui.py:456 +msgid "Submit" +msgstr "Envoyer" + +#: sat_frontends/primitivus/xmlui.py:458 sat_frontends/primitivus/xmlui.py:473 +msgid "Cancel" +msgstr "Annuler" + +#: sat_frontends/quick_frontend/constants.py:31 +msgid "Away from keyboard" +msgstr "" + +#: sat_frontends/quick_frontend/constants.py:33 +msgid "Extended away" +msgstr "" + +#: sat_frontends/quick_frontend/quick_app.py:85 +msgid "Error while trying to get autodisconnect param, ignoring: {}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_app.py:200 +#, fuzzy +msgid "Can't get profile parameter: {msg}" +msgstr "Mauvais nom de profile" + +#: sat_frontends/quick_frontend/quick_app.py:324 +msgid "Can't get namespaces map: {msg}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_app.py:330 +msgid "Can't retrieve encryption plugins: {msg}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_app.py:376 +msgid "Error while initialising bridge: {}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_app.py:662 +#, fuzzy, python-format +msgid "Can't connect profile [%s]" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/quick_frontend/quick_app.py:723 +#, fuzzy +msgid "Connected" +msgstr "Connexion..." + +#: sat_frontends/quick_frontend/quick_app.py:739 +#, fuzzy +msgid "Disconnected" +msgstr "Déconnexion..." + +#: sat_frontends/quick_frontend/quick_app.py:1154 +#, fuzzy, python-format +msgid "The contact {contact} has accepted your subscription" +msgstr "Le contact %s a accepté votre inscription" + +#: sat_frontends/quick_frontend/quick_app.py:1157 +#: sat_frontends/quick_frontend/quick_app.py:1176 +#, fuzzy +msgid "Subscription confirmation" +msgstr "désinscription confirmée pour [%s]" + +#: sat_frontends/quick_frontend/quick_app.py:1162 +#, fuzzy, python-format +msgid "The contact {contact} has refused your subscription" +msgstr "Le contact %s a refusé votre inscription" + +#: sat_frontends/quick_frontend/quick_app.py:1165 +#, fuzzy +msgid "Subscription refusal" +msgstr "demande d'inscription pour [%s]" + +#: sat_frontends/quick_frontend/quick_app.py:1172 +#, fuzzy, python-format +msgid "" +"The contact {contact} 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 ?" + +#: sat_frontends/quick_frontend/quick_app.py:1229 +#, python-format +msgid "param update: [%(namespace)s] %(name)s = %(value)s" +msgstr "Le paramètre [%(namespace)s] %(name)s vaut désormais %(value)s" + +#: sat_frontends/quick_frontend/quick_app.py:1233 +#, python-format +msgid "Changing JID to %s" +msgstr "Changement du JID pour %s" + +#: sat_frontends/quick_frontend/quick_chat.py:624 +#, fuzzy +msgid "now we print the history" +msgstr "Maintenant on affiche l'historique" + +#: sat_frontends/quick_frontend/quick_chat.py:626 +#, fuzzy +msgid " ({} messages)" +msgstr "Messages" + +#: sat_frontends/quick_frontend/quick_chat.py:683 +#, fuzzy +msgid "Can't get history: {}" +msgstr "Impossible de charger l'historique !" + +#: sat_frontends/quick_frontend/quick_chat.py:705 +msgid "Can't get encryption state: {reason}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_chat.py:775 +msgid "message encryption started with {target} using {encryption}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_chat.py:780 +msgid "message encryption stopped with {target} (was using {encryption})" +msgstr "" + +#: sat_frontends/quick_frontend/quick_chat.py:833 +msgid "<= {nick} has left the room ({count})" +msgstr "" + +#: sat_frontends/quick_frontend/quick_chat.py:837 +msgid "<=> {nick} re-entered the room ({count})" +msgstr "" + +#: sat_frontends/quick_frontend/quick_contact_list.py:611 +#, fuzzy +msgid "Trying to delete an unknow entity [{}]" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat_frontends/quick_frontend/quick_contact_list.py:664 +msgid "received presence from entity without resource: {}" +msgstr "" + +#: sat_frontends/quick_frontend/quick_contact_management.py:73 +#, fuzzy +msgid "Trying to get attribute for an unknown contact" +msgstr "Tentative d'assigner un paramètre à un profile inconnu" + +#: sat_frontends/quick_frontend/quick_contact_management.py:89 +msgid "INTERNAL ERROR: Key log.error" +msgstr "" + +#: sat_frontends/quick_frontend/quick_contact_management.py:101 +#, fuzzy, python-format +msgid "Trying to update an unknown contact: %s" +msgstr "Tentative d'accès à un profile inconnu" + +#: sat_frontends/quick_frontend/quick_games.py:84 +msgid "" +"A {game} activity between {players} has been started, but you couldn't " +"take part because your client doesn't support it." +msgstr "" + +#: sat_frontends/quick_frontend/quick_games.py:87 +msgid "{game} Game" +msgstr "" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:116 +#, fuzzy, python-format +msgid "Trying to plug an unknown profile key ({})" +msgstr "Tentative d'appel d'un profile inconnue" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:118 +#, fuzzy +msgid "Profile plugging in error" +msgstr "Mauvais nom de profile" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:133 +#, fuzzy +msgid "Can't get profile parameter" +msgstr "Mauvais nom de profile" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:144 +#, fuzzy +msgid "A profile with this name already exists" +msgstr "Ce nom de profile existe déjà" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:146 +msgid "Profile creation cancelled by backend" +msgstr "" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:148 +#, fuzzy +msgid "You profile name is not valid" +msgstr "Ce profile n'est pas utilisé" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:152 +#, fuzzy +msgid "Can't create profile ({})" +msgstr "Vous essayer de connecter un profile qui n'existe pas" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:172 +msgid "You can't connect manually and automatically at the same time" +msgstr "" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:180 +msgid "No profile selected" +msgstr "Aucun profile sélectionné" + +#: sat_frontends/quick_frontend/quick_profile_manager.py:181 +#, fuzzy +msgid "You need to create and select at least one profile before connecting" +msgstr "" +"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " +"connecter." + +#: sat_frontends/quick_frontend/quick_utils.py:40 +#, fuzzy +msgid "" +"\n" +" %prog [options]\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" +" " + +#: sat_frontends/quick_frontend/quick_utils.py:49 +msgid "Select the profile to use" +msgstr "Veuillez sélectionner le profile à utiliser" + +#: sat_frontends/tools/xmlui.py:233 +msgid "Nothing to submit" +msgstr "" + +#: sat_frontends/tools/xmlui.py:449 +msgid "XMLUI can have only one main container" +msgstr "" + +#: sat_frontends/tools/xmlui.py:514 +#, fuzzy, python-format +msgid "Unknown container [%s], using default one" +msgstr "Disposition inconnue, utilisation de celle par defaut" + +#: sat_frontends/tools/xmlui.py:527 +msgid "Internal Error, container has not _xmluiAppend method" +msgstr "" + +#: sat_frontends/tools/xmlui.py:674 +#, fuzzy, python-format +msgid "FIXME FIXME FIXME: widget type [%s] is not implemented" +msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" + +#: sat_frontends/tools/xmlui.py:678 +#, fuzzy, python-format +msgid "FIXME FIXME FIXME: type [%s] is not implemented" +msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" + +#: sat_frontends/tools/xmlui.py:696 +#, python-format +msgid "No change listener on [%s]" +msgstr "" + +#: sat_frontends/tools/xmlui.py:722 +#, fuzzy, python-format +msgid "Unknown tag [%s]" +msgstr "Type d'action inconnu" + +#: sat_frontends/tools/xmlui.py:780 +#, fuzzy +msgid "No callback_id found" +msgstr "Aucun transport trouvé" + +#: sat_frontends/tools/xmlui.py:813 +#, python-format +msgid "FIXME: XMLUI internal action [%s] is not implemented" +msgstr "" + +#: sat_frontends/tools/xmlui.py:909 sat_frontends/tools/xmlui.py:921 +#: sat_frontends/tools/xmlui.py:971 sat_frontends/tools/xmlui.py:983 +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" + +#: sat_frontends/tools/xmlui.py:915 sat_frontends/tools/xmlui.py:977 +msgid "Cancelling form" +msgstr "Annulation du formulaire" + +#: sat_frontends/tools/xmlui.py:1096 +msgid "XMLUI class already registered for {type_}, ignoring" +msgstr "" + +#: sat_frontends/tools/xmlui.py:1135 +msgid "You must register classes with registerClass before creating a XMLUI" +msgstr "" + diff -r d10748475025 -r 4b842c1fb686 i18n/fr/LC_MESSAGES/sat.mo Binary file i18n/fr/LC_MESSAGES/sat.mo has changed diff -r d10748475025 -r 4b842c1fb686 i18n/fr/LC_MESSAGES/sat.po --- a/i18n/fr/LC_MESSAGES/sat.po Thu Jun 01 21:37:34 2023 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9225 +0,0 @@ -# French translations for Libervia. -# Copyright (C) 2021 ORGANIZATION -# This file is distributed under the same license as the Libervia project. -# FIRST AUTHOR , 2021. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: 0.0.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-06-15 10:11+0200\n" -"PO-Revision-Date: 2010-03-05 19:24+1100\n" -"Last-Translator: Goffi \n" -"Language: fr\n" -"Language-Team: French \n" -"Plural-Forms: nplurals=2; plural=(n > 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" - -#: sat/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py:273 -#: sat/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:85 -#: sat/bridge/bridge_constructor/generated/dbus_bridge.py:85 -#: sat/bridge/dbus_bridge.py:747 sat_frontends/bridge/dbus_bridge.py:85 -msgid "" -"D-Bus is not launched, please see README to see instructions on how to " -"launch it" -msgstr "" - -#: sat/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:99 -#: sat/bridge/bridge_constructor/generated/dbus_bridge.py:99 -#: sat_frontends/bridge/dbus_bridge.py:99 -#, fuzzy -msgid "Unknown interface" -msgstr "Type d'action inconnu" - -#: sat/core/sat_main.py:212 -#, fuzzy -msgid "Memory initialised" -msgstr "Le flux XML est initialisé" - -#: sat/core/sat_main.py:219 -msgid "Could not initialize backend: {reason}" -msgstr "" - -#: sat/core/sat_main.py:227 -msgid "Backend is ready" -msgstr "" - -#: sat/core/sat_main.py:238 -msgid "Following profiles will be connected automatically: {profiles}" -msgstr "" - -#: sat/core/sat_main.py:251 -#, fuzzy -msgid "Can't autoconnect profile {profile}: {reason}" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat/core/sat_main.py:321 -msgid "" -"Can't import plugin [{path}]:\n" -"{error}" -msgstr "" - -#: sat/core/sat_main.py:340 -msgid "{type} type must be used with {mode} mode, ignoring plugin" -msgstr "" - -#: sat/core/sat_main.py:349 -msgid "" -"Name conflict for import name [{import_name}], can't import plugin " -"[{name}]" -msgstr "" - -#: sat/core/sat_main.py:385 -msgid "Recommended plugin not found: {}" -msgstr "" - -#: sat/core/sat_main.py:406 -msgid "Can't import plugin {name}: {error}" -msgstr "" - -#: sat/core/sat_main.py:478 -#, fuzzy -msgid "already connected !" -msgstr "Vous n'êtes pas connecté !" - -#: sat/core/sat_main.py:495 -msgid "not connected !" -msgstr "Vous n'êtes pas connecté !" - -#: sat/core/sat_main.py:591 -msgid "Trying to remove reference to a client not referenced" -msgstr "" - -#: sat/core/sat_main.py:604 -msgid "running app" -msgstr "Lancement de l'application" - -#: sat/core/sat_main.py:608 -msgid "stopping app" -msgstr "Arrêt de l'application" - -#: sat/core/sat_main.py:646 -msgid "profile_key must not be empty" -msgstr "" - -#: sat/core/sat_main.py:666 -msgid "Unexpected error: {failure_}" -msgstr "" - -#: sat/core/sat_main.py:921 -msgid "asking connection status for a non-existant profile" -msgstr "demande de l'état de connexion pour un profile qui n'existe pas" - -#: sat/core/sat_main.py:1020 -#, fuzzy, python-format -msgid "subsciption request [%(subs_type)s] for %(jid)s" -msgstr "demande d'inscription [%(type)s] pour %(jid)s" - -#: sat/core/sat_main.py:1162 -msgid "Can't find features for service {service_jid}, ignoring" -msgstr "" - -#: sat/core/sat_main.py:1221 -msgid "Can't retrieve {full_jid} infos, ignoring" -msgstr "" - -#: sat/core/sat_main.py:1292 -msgid "Trying to remove an unknow progress callback" -msgstr "Tentative d'effacement d'une callback de progression inconnue." - -#: sat/core/sat_main.py:1382 -#, fuzzy -msgid "id already registered" -msgstr "Vous êtes maintenant désinscrit" - -#: sat/core/sat_main.py:1424 -#, fuzzy -msgid "trying to launch action with a non-existant profile" -msgstr "Tentative d'ajout d'un contact à un profile inexistant" - -#: sat/core/sat_main.py:1520 -#, fuzzy -msgid "A menu with the same path and type already exists" -msgstr "Ce nom de profile existe déjà" - -#: sat/core/sat_main.py:1619 -#, fuzzy -msgid "help_string" -msgstr "enregistrement" - -#: sat/core/xmpp.py:196 -#, fuzzy -msgid "Can't parse port value, using default value" -msgstr "Pas de modèle de paramètres, utilisation du modèle par défaut" - -#: sat/core/xmpp.py:223 -msgid "We'll use the stable resource {resource}" -msgstr "" - -#: sat/core/xmpp.py:255 -msgid "setting plugins parents" -msgstr "Configuration des parents des extensions" - -#: sat/core/xmpp.py:275 -#, fuzzy -msgid "Plugins initialisation error" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/core/xmpp.py:297 -msgid "Error while disconnecting: {}" -msgstr "" - -#: sat/core/xmpp.py:301 -#, fuzzy -msgid "{profile} identified" -msgstr "Aucun profile sélectionné" - -#: sat/core/xmpp.py:309 -msgid "XML stream is initialized" -msgstr "Le flux XML est initialisé" - -#: sat/core/xmpp.py:317 -#, fuzzy, python-format -msgid "********** [{profile}] CONNECTED **********" -msgstr "********** [%s] CONNECTÉ **********" - -#: sat/core/xmpp.py:343 -#, python-format -msgid "ERROR: XMPP connection failed for profile '%(profile)s': %(reason)sprofile" -msgstr "" - -#: sat/core/xmpp.py:398 -msgid "stopping connection because of network disabled" -msgstr "" - -#: sat/core/xmpp.py:421 -msgid "network is available, trying to connect" -msgstr "" - -#: sat/core/xmpp.py:445 -#, fuzzy, python-format -msgid "********** [{profile}] DISCONNECTED **********" -msgstr "********** [%s] CONNECTÉ **********" - -#: sat/core/xmpp.py:464 -msgid "" -"Your server certificate is not valid (its identity can't be checked).\n" -"\n" -"This should never happen and may indicate that somebody is trying to spy " -"on you.\n" -"Please contact your server administrator." -msgstr "" - -#: sat/core/xmpp.py:515 -#, fuzzy -msgid "Disconnecting..." -msgstr "Déconnexion..." - -#: sat/core/xmpp.py:688 -#, fuzzy, python-format -msgid "Sending message (type {type}, to {to})" -msgstr "Envoi du message jabber à %s" - -#: sat/core/xmpp.py:696 -msgid "" -"Triggers, storage and echo have been inhibited by the 'send_only' " -"parameter" -msgstr "" - -#: sat/core/xmpp.py:762 -#, fuzzy, python-format -msgid "No message found" -msgstr "message reçu de: %s" - -#: sat/core/xmpp.py:814 -msgid "invalid data used for host: {data}" -msgstr "" - -#: sat/core/xmpp.py:839 -msgid "" -"Certificate validation is deactivated, this is unsecure and somebody may " -"be spying on you. If you have no good reason to disable certificate " -"validation, please activate \"Check certificate\" in your settings in " -"\"Connection\" tab." -msgstr "" - -#: sat/core/xmpp.py:843 -msgid "Security notice" -msgstr "" - -#: sat/core/xmpp.py:978 -msgid "The requested entry point ({entry_point}) is not available" -msgstr "" - -#: sat/core/xmpp.py:1016 -msgid "" -"Plugin {current_name} is needed for {entry_name}, but it doesn't handle " -"component mode" -msgstr "" - -#: sat/core/xmpp.py:1024 -msgid "invalid plugin mode" -msgstr "" - -#: sat/core/xmpp.py:1128 -msgid "parseMessage used with a non stanza, ignoring: {xml}" -msgstr "" - -#: sat/core/xmpp.py:1140 -msgid "received with a wrong namespace: {xml}" -msgstr "" - -#: sat/core/xmpp.py:1226 -#, fuzzy, python-format -msgid "got message from: {from_}" -msgstr "message reçu de: %s" - -#: sat/core/xmpp.py:1341 -msgid "There's no subscription between you and [{}]!" -msgstr "" - -#: sat/core/xmpp.py:1346 -msgid "You are not subscribed to [{}]!" -msgstr "" - -#: sat/core/xmpp.py:1348 -msgid "[{}] is not subscribed to you!" -msgstr "" - -#: sat/core/xmpp.py:1384 -msgid "our server support roster versioning, we use it" -msgstr "" - -#: sat/core/xmpp.py:1390 -msgid "no roster in cache, we start fresh" -msgstr "" - -#: sat/core/xmpp.py:1394 -msgid "We have roster v{version} in cache" -msgstr "" - -#: sat/core/xmpp.py:1405 -msgid "our server doesn't support roster versioning" -msgstr "" - -#: sat/core/xmpp.py:1462 -msgid "adding {entity} to roster" -msgstr "" - -#: sat/core/xmpp.py:1486 -#, fuzzy, python-format -msgid "removing {entity} from roster" -msgstr "supppression du contact %s" - -#: sat/core/xmpp.py:1640 -#, 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/core/xmpp.py:1724 -#, fuzzy -msgid "sending automatic \"from\" subscription request" -msgstr "envoi automatique de la demande d'inscription \"to\"" - -#: sat/core/xmpp.py:1732 -#, python-format -msgid "subscription approved for [%s]" -msgstr "inscription approuvée pour [%s]" - -#: sat/core/xmpp.py:1736 -#, fuzzy, python-format -msgid "unsubscription confirmed for [%s]" -msgstr "demande de désinscription pour [%s]" - -#: sat/core/xmpp.py:1741 -#, fuzzy, python-format -msgid "subscription request from [%s]" -msgstr "inscription approuvée pour [%s]" - -#: sat/core/xmpp.py:1747 -#, fuzzy -msgid "sending automatic subscription acceptance" -msgstr "envoi automatique de la demande d'inscription \"to\"" - -#: sat/core/xmpp.py:1759 -#, python-format -msgid "unsubscription asked for [%s]" -msgstr "demande de désinscription pour [%s]" - -#: sat/core/xmpp.py:1763 -#, fuzzy -msgid "automatic contact deletion" -msgstr "Sélection du contrat" - -#: sat/memory/cache.py:69 -msgid "Can't read metadata file at {path}" -msgstr "" - -#: sat/memory/cache.py:80 -msgid "Invalid cache metadata at {path}" -msgstr "" - -#: sat/memory/cache.py:87 -msgid "cache {cache_file!r} references an inexisting file: {filepath!r}" -msgstr "" - -#: sat/memory/cache.py:102 -msgid "following file is missing while purging cache: {path}" -msgstr "" - -#: sat/memory/cache.py:200 -msgid "missing filename for cache {uid!r}" -msgstr "" - -#: sat/memory/cache.py:207 -msgid "missing file referenced in cache {uid!r}: {filename}" -msgstr "" - -#: sat/memory/disco.py:95 -msgid "" -"no feature/identity found in disco element (hash: {cap_hash}), ignoring: " -"{xml}" -msgstr "" - -#: sat/memory/disco.py:274 -#, python-format -msgid "Error while requesting [%(jid)s]: %(error)s" -msgstr "" - -#: sat/memory/disco.py:338 -msgid "received an item without jid" -msgstr "" - -#: sat/memory/disco.py:410 -msgid "Capability hash generated: [{cap_hash}]" -msgstr "" - -#: sat/memory/disco.py:459 -msgid "invalid item (no jid)" -msgstr "" - -#: sat/memory/encryption.py:71 -msgid "Could not restart {namespace!r} encryption with {entity}: {err}" -msgstr "" - -#: sat/memory/encryption.py:74 -msgid "encryption sessions restored" -msgstr "" - -#: sat/memory/encryption.py:116 -msgid "Encryption plugin registered: {name}" -msgstr "" - -#: sat/memory/encryption.py:127 -msgid "Can't find requested encryption plugin: {namespace}" -msgstr "" - -#: sat/memory/encryption.py:148 -msgid "Can't find a plugin with the name \"{name}\"." -msgstr "" - -#: sat/memory/encryption.py:213 -msgid "No encryption plugin is registered, an encryption session can't be started" -msgstr "" - -#: sat/memory/encryption.py:226 -msgid "Session with {bare_jid} is already encrypted with {name}. Nothing to do." -msgstr "" - -#: sat/memory/encryption.py:237 -msgid "" -"Session with {bare_jid} is already encrypted with {name}. Please stop " -"encryption session before changing algorithm." -msgstr "" - -#: sat/memory/encryption.py:249 -msgid "No resource found for {destinee}, can't encrypt with {name}" -msgstr "" - -#: sat/memory/encryption.py:251 -msgid "No resource specified to encrypt with {name}, using {destinee}." -msgstr "" - -#: sat/memory/encryption.py:257 -msgid "{name} encryption must be used with bare jids." -msgstr "" - -#: sat/memory/encryption.py:261 -msgid "Encryption session has been set for {entity_jid} with {encryption_name}" -msgstr "" - -#: sat/memory/encryption.py:268 -msgid "" -"Encryption session started: your messages with {destinee} are now end to " -"end encrypted using {name} algorithm." -msgstr "" - -#: sat/memory/encryption.py:273 -msgid "Message are encrypted only for {nb_devices} device(s): {devices_list}." -msgstr "" - -#: sat/memory/encryption.py:291 -msgid "There is no encryption session with this entity." -msgstr "" - -#: sat/memory/encryption.py:295 -msgid "" -"The encryption session is not run with the expected plugin: encrypted " -"with {current_name} and was expecting {expected_name}" -msgstr "" - -#: sat/memory/encryption.py:304 -msgid "" -"There is a session for the whole entity (i.e. all devices of the entity)," -" not a directed one. Please use bare jid if you want to stop the whole " -"encryption with this entity." -msgstr "" - -#: sat/memory/encryption.py:312 -msgid "There is no directed session with this entity." -msgstr "" - -#: sat/memory/encryption.py:327 -msgid "encryption session stopped with entity {entity}" -msgstr "" - -#: sat/memory/encryption.py:335 -msgid "" -"Encryption session finished: your messages with {destinee} are NOT end to" -" end encrypted anymore.\n" -"Your server administrators or {destinee} server administrators will be " -"able to read them." -msgstr "" - -#: sat/memory/encryption.py:389 sat/memory/encryption.py:397 -#: sat/memory/encryption.py:404 -#, fuzzy -msgid "Encryption" -msgstr "Connexion..." - -#: sat/memory/encryption.py:389 -msgid "unencrypted (plain text)" -msgstr "" - -#: sat/memory/encryption.py:392 -msgid "End encrypted session" -msgstr "" - -#: sat/memory/encryption.py:400 -msgid "Start {name} session" -msgstr "" - -#: sat/memory/encryption.py:404 -msgid "⛨ {name} trust" -msgstr "" - -#: sat/memory/encryption.py:407 -msgid "Manage {name} trust" -msgstr "" - -#: sat/memory/encryption.py:470 -msgid "Starting e2e session with {peer_jid} as we receive encrypted messages" -msgstr "" - -#: sat/memory/memory.py:230 -msgid "Memory manager init" -msgstr "Initialisation du gestionnaire de mémoire" - -#: sat/memory/memory.py:249 -#, fuzzy -msgid "Loading default params template" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat/memory/memory.py:281 -#, python-format -msgid "Parameters loaded from file: %s" -msgstr "" - -#: sat/memory/memory.py:284 -#, fuzzy, python-format -msgid "Can't load parameters from file: %s" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat/memory/memory.py:299 -#, fuzzy, python-format -msgid "Parameters saved to file: %s" -msgstr "Échec de la désinscription: %s" - -#: sat/memory/memory.py:302 -#, fuzzy, python-format -msgid "Can't save parameters to file: %s" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat/memory/memory.py:404 -msgid "Authentication failure of profile {profile}" -msgstr "" - -#: sat/memory/memory.py:431 -#, fuzzy, python-format -msgid "[%s] Profile session purge" -msgstr "Ce profile n'est pas utilisé" - -#: sat/memory/memory.py:437 -#, python-format -msgid "Trying to purge roster status cache for a profile not in memory: [%s]" -msgstr "" - -#: sat/memory/memory.py:451 -msgid "requesting no profiles at all" -msgstr "" - -#: sat/memory/memory.py:508 -msgid "Can't find component {component} entry point" -msgstr "" - -#: sat/memory/memory.py:996 -msgid "Need a bare jid to delete all resources" -msgstr "" - -#: sat/memory/memory.py:1028 -#, python-format -msgid "Trying to encrypt a value for %s while the personal key is undefined!" -msgstr "" - -#: sat/memory/memory.py:1048 -#, python-format -msgid "Trying to decrypt a value for %s while the personal key is undefined!" -msgstr "" - -#: sat/memory/memory.py:1069 -#, python-format -msgid "Personal data (%(ns)s, %(key)s) has been successfuly encrypted" -msgstr "" - -#: sat/memory/memory.py:1097 -msgid "Asking waiting subscriptions for a non-existant profile" -msgstr "Demande des inscriptions en attente pour un profile inexistant" - -#: sat/memory/memory.py:1218 -msgid "invalid permission" -msgstr "" - -#: sat/memory/memory.py:1249 -#, fuzzy -msgid "unknown access type: {type}" -msgstr "Type d'action inconnu" - -#: sat/memory/memory.py:1284 -msgid "You can't use path and parent at the same time" -msgstr "" - -#: sat/memory/memory.py:1288 -msgid "\"..\" or \".\" can't be used in path" -msgstr "" - -#: sat/memory/memory.py:1307 -msgid "Several directories found, this should not happen" -msgstr "" - -#: sat/memory/memory.py:1766 -msgid "Can't delete directory, it is not empty" -msgstr "" - -#: sat/memory/memory.py:1778 -msgid "deleting file {name} with hash {file_hash}" -msgstr "" - -#: sat/memory/memory.py:1787 -msgid "no reference left to {file_path}, deleting" -msgstr "" - -#: sat/memory/params.py:85 sat_frontends/primitivus/base.py:533 -msgid "General" -msgstr "Général" - -#: sat/memory/params.py:86 -#, fuzzy -msgid "Connection" -msgstr "Connexion..." - -#: sat/memory/params.py:88 -msgid "Chat history limit" -msgstr "" - -#: sat/memory/params.py:90 -msgid "Show offline contacts" -msgstr "" - -#: sat/memory/params.py:92 -msgid "Show empty groups" -msgstr "" - -#: sat/memory/params.py:95 -#, fuzzy -msgid "Connect on backend startup" -msgstr "Connexion au démarrage des frontends" - -#: sat/memory/params.py:96 -#, fuzzy -msgid "Connect on frontend startup" -msgstr "Connexion au démarrage des frontends" - -#: sat/memory/params.py:97 -#, fuzzy -msgid "Disconnect on frontend closure" -msgstr "Déconnexion à la fermeture des frontends" - -#: sat/memory/params.py:98 -msgid "Check certificate (don't uncheck if unsure)" -msgstr "" - -#: sat/memory/params.py:163 -#, fuzzy, python-format -msgid "Trying to purge cache of a profile not in memory: [%s]" -msgstr "Tentative d'appel d'un profile inconnue" - -#: sat/memory/params.py:188 -#, fuzzy -msgid "The profile name already exists" -msgstr "Ce nom de profile existe déjà" - -#: sat/memory/params.py:203 -#, fuzzy -msgid "Trying to delete an unknown profile" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat/memory/params.py:209 -#, fuzzy -msgid "Trying to delete a connected profile" -msgstr "Tentative de suppression d'un contact pour un profile inexistant" - -#: sat/memory/params.py:228 -msgid "No default profile, returning first one" -msgstr "Pas de profile par défaut, envoi du premier" - -#: sat/memory/params.py:234 -#, fuzzy -msgid "No profile exist yet" -msgstr "Aucun profile sélectionné" - -#: sat/memory/params.py:244 -#, fuzzy, python-format -msgid "Trying to access an unknown profile (%s)" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat/memory/params.py:338 -msgid "Trying to register frontends parameters with no specified app: aborted" -msgstr "" - -#: sat/memory/params.py:347 -#, python-format -msgid "Trying to register twice frontends parameters for %(app)s: abortedapp" -msgstr "" - -#: sat/memory/params.py:363 -#, 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" - -#: sat/memory/params.py:385 sat/memory/params.py:563 sat/memory/params.py:624 -#, 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 !" - -#: sat/memory/params.py:440 -#, python-format -msgid "" -"Unset parameter (%(cat)s, %(param)s) of type list will use the default " -"option '%(value)s'" -msgstr "" - -#: sat/memory/params.py:448 -#, python-format -msgid "Parameter (%(cat)s, %(param)s) of type list has no default option!" -msgstr "" - -#: sat/memory/params.py:455 -#, python-format -msgid "" -"Parameter (%(cat)s, %(param)s) of type list has more than one default " -"option!" -msgstr "" - -#: sat/memory/params.py:585 -msgid "Requesting a param for an non-existant profile" -msgstr "Demande d'un paramètre pour un profile inconnu" - -#: sat/memory/params.py:589 -#, fuzzy -msgid "Requesting synchronous param for not connected profile" -msgstr "Demande d'un paramètre pour un profile inconnu" - -#: sat/memory/params.py:633 -#, python-format -msgid "" -"Trying to get parameter '%(param)s' in category '%(cat)s' without " -"authorization!!!param" -msgstr "" - -#: sat/memory/params.py:649 -#, fuzzy -msgid "Requesting a param for a non-existant profile" -msgstr "Demande d'un paramètre pour un profile inconnu" - -#: sat/memory/params.py:962 -#, fuzzy -msgid "Trying to set parameter for an unknown profile" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat/memory/params.py:968 -#, python-format -msgid "Requesting an unknown parameter (%(category)s/%(name)s)" -msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" - -#: sat/memory/params.py:974 -msgid "" -"{profile!r} is trying to set parameter {name!r} in category {category!r} " -"without authorization!!!" -msgstr "" - -#: sat/memory/params.py:992 -msgid "" -"Trying to set parameter {name} in category {category} withan non-integer " -"value" -msgstr "" - -#: sat/memory/params.py:1011 -#, fuzzy, python-format -msgid "Setting parameter (%(category)s, %(name)s) = %(value)s" -msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" - -#: sat/memory/params.py:1043 -msgid "Trying to encrypt a password while the personal key is undefined!" -msgstr "" - -#: sat/memory/persistent.py:45 -msgid "PersistentDict can't be used before memory initialisation" -msgstr "" - -#: sat/memory/persistent.py:175 -msgid "Calling load on LazyPersistentBinaryDict while it's not needed" -msgstr "" - -#: sat/memory/sqlite.py:163 -msgid "" -"too many db tries, we abandon! Error message: {msg}\n" -"query was {query}" -msgstr "" - -#: sat/memory/sqlite.py:166 -msgid "exception while running query, retrying ({try_}): {msg}" -msgstr "" - -#: sat/memory/sqlite.py:188 -msgid "" -"too many interaction tries, we abandon! Error message: {msg}\n" -"interaction method was: {interaction}\n" -"interaction arguments were: {args}" -msgstr "" - -#: sat/memory/sqlite.py:191 -msgid "exception while running interaction, retrying ({try_}): {msg}" -msgstr "" - -#: sat/memory/sqlite.py:210 -msgid "Connecting database" -msgstr "" - -#: sat/memory/sqlite.py:223 -#, fuzzy -msgid "The database is new, creating the tables" -msgstr "Ce nom de profile existe déjà" - -#: sat/memory/sqlite.py:337 -#, fuzzy, python-format -msgid "Can't delete profile [%s]" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat/memory/sqlite.py:354 -#, fuzzy, python-format -msgid "Profile [%s] deleted" -msgstr "Aucun profile sélectionné" - -#: sat/memory/sqlite.py:370 -#, fuzzy -msgid "loading general parameters from database" -msgstr "Impossible de charger les paramètres généraux !" - -#: sat/memory/sqlite.py:385 -#, fuzzy -msgid "loading individual parameters from database" -msgstr "Impossible de charger les paramètres individuels !" - -#: sat/memory/sqlite.py:426 -#, fuzzy, python-format -msgid "Can't set general parameter (%(category)s/%(name)s) in databasecategory" -msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" - -#: sat/memory/sqlite.py:439 -#, fuzzy, python-format -msgid "" -"Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] " -"in databasecategory" -msgstr "" -"Impossible de déterminer la valeur par défaut pour " -"[%(category)s/%(name)s]: %(reason)s" - -#: sat/memory/sqlite.py:459 -msgid "Can't save following {key} in history (uid: {uid}, lang:{lang}): {value}" -msgstr "" - -#: sat/memory/sqlite.py:473 -msgid "" -"Can't save following thread in history (uid: {uid}): thread: {thread}), " -"parent:{parent}" -msgstr "" - -#: sat/memory/sqlite.py:498 -msgid "" -"Can't save following message in history: from [{from_jid}] to [{to_jid}] " -"(uid: {uid})" -msgstr "" - -#: sat/memory/sqlite.py:701 -msgid "" -"Can't {operation} data in database for namespace " -"{namespace}{and_key}{for_profile}: {msg}" -msgstr "" - -#: sat/memory/sqlite.py:752 -msgid "" -"getting {type}{binary} private values from database for namespace " -"{namespace}{keys}" -msgstr "" - -#: sat/memory/sqlite.py:986 -msgid "Can't save file metadata for [{profile}]: {reason}" -msgstr "" - -#: sat/memory/sqlite.py:1025 -msgid "table not updated, probably due to race condition, trying again ({tries})" -msgstr "" - -#: sat/memory/sqlite.py:1027 -msgid "Can't update file table" -msgstr "" - -#: sat/memory/sqlite.py:1132 -msgid "" -"Your local schema is up-to-date, but database versions mismatch, fixing " -"it..." -msgstr "" - -#: sat/memory/sqlite.py:1142 -msgid "" -"There is a schema mismatch, but as we are on a dev version, database will" -" be updated" -msgstr "" - -#: sat/memory/sqlite.py:1146 -msgid "" -"schema version is up-to-date, but local schema differ from expected " -"current schema" -msgstr "" - -#: sat/memory/sqlite.py:1149 -#, python-format -msgid "" -"Here are the commands that should fix the situation, use at your own risk" -" (do a backup before modifying database), you can go to SàT's MUC room at" -" sat@chat.jabberfr.org for help\n" -"### SQL###\n" -"%s\n" -"### END SQL ###\n" -msgstr "" - -#: sat/memory/sqlite.py:1153 -msgid "" -"You database version is higher than the one used in this SàT version, are" -" you using several version at the same time? We can't run SàT with this " -"database." -msgstr "" - -#: sat/memory/sqlite.py:1161 -msgid "" -"Database content needs a specific processing, local database will be " -"updated" -msgstr "" - -#: sat/memory/sqlite.py:1163 -msgid "Database schema has changed, local database will be updated" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:91 -#, fuzzy -msgid "Add D-Bus management to Ad-Hoc commands" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_adhoc_dbus.py:98 -#, fuzzy -msgid "plugin Ad-Hoc D-Bus initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_adhoc_dbus.py:127 -msgid "Media Players" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:255 -#, fuzzy -msgid "Command selection" -msgstr "Sélection du contrat" - -#: sat/plugins/plugin_adhoc_dbus.py:298 -#, fuzzy -msgid "Updated" -msgstr "mise à jour de %s" - -#: sat/plugins/plugin_adhoc_dbus.py:302 -msgid "Command sent" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:367 -msgid "Can't retrieve remote controllers on {device_jid}: {reason}" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:405 -#, fuzzy -msgid "No media player found." -msgstr "Aucune donnée trouvée" - -#: sat/plugins/plugin_adhoc_dbus.py:409 sat/plugins/plugin_adhoc_dbus.py:451 -#, fuzzy -msgid "Media Player Selection" -msgstr "Sélection du contrat" - -#: sat/plugins/plugin_adhoc_dbus.py:414 -msgid "Ignoring MPRIS bus without suffix" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:428 -msgid "missing media_player value" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:431 -msgid "" -"Media player ad-hoc command trying to use non MPRIS bus. Hack attempt? " -"Refused bus: {bus_name}" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:434 -msgid "Invalid player name." -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:440 -msgid "Can't get D-Bus proxy: {reason}" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:441 -msgid "Media player is not available anymore" -msgstr "" - -#: sat/plugins/plugin_adhoc_dbus.py:460 -msgid "Can't retrieve attribute {name}: {reason}" -msgstr "" - -#: sat/plugins/plugin_blog_import.py:45 -msgid "" -"Blog import management:\n" -"This plugin manage the different blog importers which can register to it," -" and handle generic importing tasks." -msgstr "" - -#: sat/plugins/plugin_blog_import.py:64 -#, fuzzy -msgid "plugin Blog Import initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:58 -msgid "Blog importer for Dokuwiki blog engine." -msgstr "" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:61 -msgid "import posts from Dokuwiki blog engine" -msgstr "" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:63 -msgid "" -"This importer handle Dokuwiki blog engine.\n" -"\n" -"To use it, you need an admin access to a running Dokuwiki website\n" -"(local or on the Internet). The importer retrieves the data using\n" -"the XMLRPC Dokuwiki API.\n" -"\n" -"You can specify a namespace (that could be a namespace directory\n" -"or a single post) or leave it empty to use the root namespace \"/\"\n" -"and import all the posts.\n" -"\n" -"You can specify a new media repository to modify the internal\n" -"media links and make them point to the URL of your choice, but\n" -"note that the upload is not done automatically: a temporary\n" -"directory will be created on your local drive and you will\n" -"need to upload it yourself to your repository via SSH or FTP.\n" -"\n" -"Following options are recognized:\n" -"\n" -"location: DokuWiki site URL\n" -"user: DokuWiki admin user\n" -"passwd: DokuWiki admin password\n" -"namespace: DokuWiki namespace to import (default: root namespace \"/\")\n" -"media_repo: URL to the new remote media repository (default: none)\n" -"limit: maximal number of posts to import (default: 100)\n" -"\n" -"Example of usage (with jp frontend):\n" -"\n" -"jp import dokuwiki -p dave --pwd xxxxxx --connect\n" -" http://127.0.1.1 -o user souliane -o passwd qwertz\n" -" -o namespace public:2015:10\n" -" -o media_repo http://media.diekulturvermittlung.at\n" -"\n" -"This retrieves the 100 last blog posts from http://127.0.1.1 that\n" -"are inside the namespace \"public:2015:10\" using the Dokuwiki user\n" -"\"souliane\", and it imports them to sat profile dave's microblog node.\n" -"Internal Dokuwiki media that were hosted on http://127.0.1.1 are now\n" -"pointing to http://media.diekulturvermittlung.at.\n" -msgstr "" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:351 -#, fuzzy -msgid "plugin Dokuwiki Import initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:383 -msgid "" -"DokuWiki media files will be *downloaded* to {temp_dir} - to finish the " -"import you have to upload them *manually* to {media_repo}" -msgstr "" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:389 -msgid "" -"DokuWiki media files will be *uploaded* to the XMPP server. Hyperlinks to" -" these media may not been updated though." -msgstr "" - -#: sat/plugins/plugin_blog_import_dokuwiki.py:393 -msgid "" -"DokuWiki media files will *stay* on {location} - some of them may be " -"protected by DokuWiki ACL and will not be accessible." -msgstr "" - -#: sat/plugins/plugin_blog_import_dotclear.py:42 -msgid "Blog importer for Dotclear blog engine." -msgstr "" - -#: sat/plugins/plugin_blog_import_dotclear.py:45 -msgid "import posts from Dotclear blog engine" -msgstr "" - -#: sat/plugins/plugin_blog_import_dotclear.py:47 -msgid "" -"This importer handle Dotclear blog engine.\n" -"\n" -"To use it, you'll need to export your blog to a flat file.\n" -"You must go in your admin interface and select Plugins/Maintenance then " -"Backup.\n" -"Export only one blog if you have many, i.e. select \"Download database of" -" current blog\"\n" -"Depending on your configuration, your may need to use Import/Export " -"plugin and export as a flat file.\n" -"\n" -"location: you must use the absolute path to your backup for the location " -"parameter\n" -msgstr "" - -#: sat/plugins/plugin_blog_import_dotclear.py:266 -#, fuzzy -msgid "plugin Dotclear Import initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_comp_file_sharing.py:69 -msgid "Component hosting and sharing files" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:79 -msgid "" -"You are over quota, your maximum allowed size is {quota} and you are " -"already using {used_space}, you can't upload {file_size} more." -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:350 -#, fuzzy -msgid "File Sharing initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_comp_file_sharing.py:431 -#: sat/plugins/plugin_comp_file_sharing_management.py:422 -msgid "Can't create thumbnail: {reason}" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:454 -msgid "Reusing already generated hash" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:485 -msgid "Can't get thumbnail for {final_path}: {e}" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:574 -msgid "{peer_jid} is trying to access an unauthorized file: {name}" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing.py:582 -msgid "no matching file found ({file_data})" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:43 -msgid "" -"Experimental handling of file management for file sharing. This plugins " -"allows to change permissions of stored files/directories or remove them." -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:72 -#, fuzzy -msgid "File Sharing Management plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_comp_file_sharing_management.py:185 -#, fuzzy -msgid "file not found" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_comp_file_sharing_management.py:187 -#: sat/plugins/plugin_comp_file_sharing_management.py:192 -#: sat/plugins/plugin_comp_file_sharing_management.py:474 -msgid "forbidden" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:191 -msgid "Only owner can manage files" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:258 -msgid "Please select permissions for this directory" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:260 -msgid "Please select permissions for this file" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:305 -msgid "Can't use read_allowed values: {reason}" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:332 -msgid "management session done" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:358 -msgid "" -"Are you sure to delete directory {name} and all files and directories " -"under it?" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:362 -#, fuzzy, python-format -msgid "Are you sure to delete file {name}?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat/plugins/plugin_comp_file_sharing_management.py:387 -#, fuzzy, python-format -msgid "file deleted" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_comp_file_sharing_management.py:465 -msgid "thumbnails generated" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:481 -msgid "You are currently using {size_used} on {size_quota}" -msgstr "" - -#: sat/plugins/plugin_comp_file_sharing_management.py:484 -msgid "unlimited quota" -msgstr "" - -#: sat/plugins/plugin_dbg_manhole.py:39 -msgid "Debug plugin to have a telnet server" -msgstr "" - -#: sat/plugins/plugin_dbg_manhole.py:53 -msgid "" -"/!\\ Manhole debug server activated, be sure to not use it in production," -" this is dangerous /!\\" -msgstr "" - -#: sat/plugins/plugin_dbg_manhole.py:55 -msgid "You can connect to manhole server using telnet on port {port}" -msgstr "" - -#: sat/plugins/plugin_exp_command_export.py:39 -#, fuzzy -msgid "Implementation of command export" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_exp_command_export.py:92 -#, fuzzy -msgid "Plugin command export initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_events.py:50 -msgid "Experimental implementation of XMPP events management" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:60 -#, fuzzy -msgid "Event plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_events.py:177 -msgid "no src found for image" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:187 -#, fuzzy -msgid "no {uri_type} element found!" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_exp_events.py:189 -msgid "incomplete {uri_type} element" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:191 -msgid "bad {uri_type} element" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:231 -#, fuzzy -msgid "No event element has been found" -msgstr "Aucune donnée trouvée" - -#: sat/plugins/plugin_exp_events.py:233 -msgid "No event with this id has been found" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:290 -msgid "event_id must be set" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:333 -msgid "The given URI is not valid: {uri}" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:354 -#: sat/plugins/plugin_exp_list_of_interest.py:100 -#, fuzzy -msgid "requested node already exists" -msgstr "Ce nom de profile existe déjà" - -#: sat/plugins/plugin_exp_events.py:373 -msgid "missing node" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:426 -msgid "No event found in item {item_id}, ignoring" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:519 -msgid "no data found for {item_id} (service: {service}, node: {node})" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:542 sat/plugins/plugin_exp_events.py:623 -msgid "\"XEP-0277\" (blog) plugin is needed for this feature" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:548 -msgid "got event data" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:579 -msgid "affiliation set on blog and comments nodes" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:619 -msgid "\"Invitations\" plugin is needed for this feature" -msgstr "" - -#: sat/plugins/plugin_exp_events.py:632 -#, fuzzy -msgid "invitation created" -msgstr "Connexion..." - -#: sat/plugins/plugin_exp_invitation.py:44 -#, fuzzy -msgid "Experimental handling of invitations" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_exp_invitation.py:57 -#, fuzzy -msgid "Invitation plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_exp_invitation.py:251 -msgid "Can't get item linked with invitation: {reason}" -msgstr "" - -#: sat/plugins/plugin_exp_invitation.py:256 -msgid "Invitation was linking to a non existing item" -msgstr "" - -#: sat/plugins/plugin_exp_invitation.py:262 -msgid "Can't retrieve namespace of invitation: {reason}" -msgstr "" - -#: sat/plugins/plugin_exp_invitation.py:281 -msgid "Bad invitation, ignoring" -msgstr "" - -#: sat/plugins/plugin_exp_invitation.py:321 -msgid "No handler for namespace \"{namespace}\", invitation ignored" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_file.py:39 -msgid "Experimental handling of invitations for file sharing" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_file.py:46 -#, fuzzy -msgid "File Sharing Invitation plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_exp_invitation_file.py:85 -#: sat/plugins/plugin_exp_invitation_file.py:92 -msgid "file sharing" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_file.py:87 -msgid "photo album" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_file.py:93 -msgid "" -"{profile} has received an invitation for a files repository " -"({type_human}) with namespace {sharing_ns!r} at path [{path}]" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_pubsub.py:42 -msgid "Invitations for pubsub based features" -msgstr "" - -#: sat/plugins/plugin_exp_invitation_pubsub.py:49 -#, fuzzy -msgid "Pubsub Invitation plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_jingle_stream.py:52 -msgid "Jingle Stream plugin" -msgstr "" - -#: sat/plugins/plugin_exp_jingle_stream.py:55 -#, fuzzy, python-format -msgid "{peer} wants to send you a stream, do you accept ?" -msgstr "" -"Le contact %(jid)s veut vous envoyer le fichier %(filename)s\n" -"Êtes vous d'accord ?" - -#: sat/plugins/plugin_exp_jingle_stream.py:56 -#, fuzzy -msgid "Stream Request" -msgstr "Gestion des paramètres" - -#: sat/plugins/plugin_exp_jingle_stream.py:123 -msgid "stream can't be used with multiple consumers" -msgstr "" - -#: sat/plugins/plugin_exp_jingle_stream.py:170 -#, fuzzy -msgid "No client connected, can't send data" -msgstr "Connexion du client SOCKS 5 démarrée" - -#: sat/plugins/plugin_exp_jingle_stream.py:180 -#, fuzzy -msgid "Plugin Stream initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_jingle_stream.py:270 -msgid "given port is invalid" -msgstr "" - -#: sat/plugins/plugin_exp_lang_detect.py:45 -msgid "Detect and set message language when unknown" -msgstr "" - -#: sat/plugins/plugin_exp_lang_detect.py:48 -#: sat/plugins/plugin_misc_watched.py:43 sat/plugins/plugin_xep_0249.py:73 -msgid "Misc" -msgstr "Divers" - -#: sat/plugins/plugin_exp_lang_detect.py:50 -msgid "language detection" -msgstr "" - -#: sat/plugins/plugin_exp_lang_detect.py:66 -#, fuzzy -msgid "Language detection plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_exp_list_of_interest.py:46 -msgid "Experimental handling of interesting XMPP locations" -msgstr "" - -#: sat/plugins/plugin_exp_list_of_interest.py:56 -#, fuzzy -msgid "List of Interest plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_exp_list_of_interest.py:287 -msgid "Missing interest element: {xml}" -msgstr "" - -#: sat/plugins/plugin_exp_parrot.py:40 -msgid "Implementation of parrot mode (repeat messages between 2 entities)" -msgstr "" - -#: sat/plugins/plugin_exp_parrot.py:56 -#, fuzzy -msgid "Plugin Parrot initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_parrot.py:63 sat/plugins/plugin_xep_0045.py:150 -#: sat/plugins/plugin_xep_0048.py:102 sat/plugins/plugin_xep_0092.py:61 -#: sat/plugins/plugin_xep_0199.py:56 sat/plugins/plugin_xep_0249.py:95 -#: sat/plugins/plugin_xep_0384.py:476 -msgid "Text commands not available" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_admin.py:40 -msgid "" -"\\Implementation of Pubsub Administrator\n" -"This allows a pubsub administrator to overwrite completly items, " -"including publisher.\n" -"Specially useful when importing a node." -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:40 -msgid "Experimental plugin to launch on action on Pubsub notifications" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:56 -#, fuzzy -msgid "PubSub Hook initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_exp_pubsub_hook.py:93 -msgid "node manager already set for {node}" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:101 -msgid "node manager installed on {node}" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:107 -#, fuzzy -msgid "trying to remove a {node} without hook" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat/plugins/plugin_exp_pubsub_hook.py:112 -msgid "hook removed" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:114 -msgid "node still needed for an other hook" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:119 -msgid "{hook_type} is not handled" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:123 -#: sat/plugins/plugin_exp_pubsub_hook.py:167 -msgid "{hook_type} hook type not implemented yet" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:139 -msgid "{persistent} hook installed on {node} for {profile}" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:140 -msgid "persistent" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:140 -msgid "temporary" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:173 -msgid "Can't load Pubsub hook at node {node}, it will be removed: {reason}" -msgstr "" - -#: sat/plugins/plugin_exp_pubsub_hook.py:185 -msgid "Error while running Pubsub hook for node {node}: {msg}" -msgstr "" - -#: sat/plugins/plugin_import.py:41 -msgid "Generic import plugin, base for specialized importers" -msgstr "" - -#: sat/plugins/plugin_import.py:49 -#, fuzzy -msgid "plugin Import initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_import.py:67 -msgid "initializing {name} import handler" -msgstr "" - -#: sat/plugins/plugin_import.py:158 -msgid "invalid json option: {option}" -msgstr "" - -#: sat/plugins/plugin_import.py:296 -msgid "uploading subitems" -msgstr "" - -#: sat/plugins/plugin_import.py:327 -#, fuzzy -msgid "An {handler_name} importer with the name {name} already exist" -msgstr "Ce nom de profile existe déjà" - -#: sat/plugins/plugin_merge_req_mercurial.py:37 -msgid "Merge request handler for Mercurial" -msgstr "" - -#: sat/plugins/plugin_merge_req_mercurial.py:40 -msgid "handle Mercurial repository" -msgstr "" - -#: sat/plugins/plugin_merge_req_mercurial.py:71 -msgid "Mercurial merge request handler initialization" -msgstr "" - -#: sat/plugins/plugin_merge_req_mercurial.py:75 -msgid "Mercurial executable (hg) not found, can't use Mercurial handler" -msgstr "" - -#: sat/plugins/plugin_merge_req_mercurial.py:116 -msgid "invalid changeset signature" -msgstr "" - -#: sat/plugins/plugin_merge_req_mercurial.py:136 -msgid "unexpected time data: {data}" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:50 -msgid "Libervia account creation" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:75 -msgid "" -"Welcome to Libervia, the web interface of Salut à Toi.\n" -"\n" -"Your account on {domain} has been successfully created.\n" -"This is a demonstration version to show you the current status of the " -"project.\n" -"It is still under development, please keep it in mind!\n" -"\n" -"Here is your connection information:\n" -"\n" -"Login on {domain}: {profile}\n" -"Jabber ID (JID): {jid}\n" -"Your password has been chosen by yourself during registration.\n" -"\n" -"In the beginning, you have nobody to talk to. To find some contacts, you " -"may use the users' directory:\n" -" - make yourself visible in \"Service / Directory subscription\".\n" -" - search for people with \"Contacts\" / Search directory\".\n" -"\n" -"Any feedback welcome. Thank you!\n" -"\n" -"Salut à Toi association\n" -"https://www.salut-a-toi.org\n" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:109 -#, fuzzy -msgid "Plugin Account initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_account.py:294 -msgid "Failed to send account creation confirmation to {email}: {msg}" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:313 -msgid "New Libervia account created" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:339 -msgid "Your Libervia account has been created" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:363 -msgid "xmpp_domain needs to be set in sat.conf. Using \"{default}\" meanwhile" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:379 -msgid "Manage your account" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:385 -#, fuzzy -msgid "Change your password" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat/plugins/plugin_misc_account.py:387 -msgid "Current profile password" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:389 -#, fuzzy -msgid "New password" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat/plugins/plugin_misc_account.py:391 -#, fuzzy -msgid "New password (again)" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat/plugins/plugin_misc_account.py:431 sat/stdui/ui_profile_manager.py:73 -#, fuzzy -msgid "The provided profile password doesn't match." -msgstr "Le fichier [%s] n'existe pas !" - -#: sat/plugins/plugin_misc_account.py:432 -msgid "Attempt failure" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:477 -msgid "The values entered for the new password are not equal." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:495 -#, fuzzy -msgid "Change your password?" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat/plugins/plugin_misc_account.py:500 -msgid "" -"Note for advanced users: this will actually change both your SàT profile " -"password AND your XMPP account password." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:504 -msgid "Continue with changing the password?" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:528 -#: sat/plugins/plugin_misc_register_account.py:133 -#, fuzzy -msgid "Confirmation" -msgstr "Connexion..." - -#: sat/plugins/plugin_misc_account.py:529 -msgid "Your password has been changed." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:533 -#: sat/plugins/plugin_misc_account.py:606 -#: sat/plugins/plugin_misc_account.py:716 sat_frontends/primitivus/base.py:790 -#: sat_frontends/primitivus/base.py:831 -#: sat_frontends/quick_frontend/quick_profile_manager.py:133 -msgid "Error" -msgstr "Erreur" - -#: sat/plugins/plugin_misc_account.py:535 -#, python-format -msgid "Your password could not be changed: %s" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:548 -#, fuzzy -msgid "Delete your account?" -msgstr "Enregistrement d'un nouveau compte" - -#: sat/plugins/plugin_misc_account.py:551 -msgid "" -"If you confirm this dialog, you will be disconnected and then your XMPP " -"account AND your SàT profile will both be DELETED." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:555 -msgid "contact list, messages history, blog posts and commentsGROUPBLOG" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:558 -msgid "contact list and messages history" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:561 -#, python-format -msgid "" -"All your data stored on %(server)s, including your %(target)s will be " -"erased." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:567 -#: sat/plugins/plugin_misc_account.py:642 -#: sat/plugins/plugin_misc_account.py:658 -#: sat/plugins/plugin_misc_account.py:674 -msgid "" -"There is no other confirmation dialog, this is the very last one! Are you" -" sure?" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:608 -#, python-format -msgid "Your XMPP account could not be deleted: %s" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:628 -msgid "Delete all your (micro-)blog posts and comments?" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:632 -msgid "" -"If you confirm this dialog, all the (micro-)blog data you submitted will " -"be erased." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:637 -msgid "These are the public and private posts and comments you sent to any group." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:649 -msgid "Delete all your (micro-)blog posts?" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:653 -msgid "" -"If you confirm this dialog, all the public and private posts you sent to " -"any group will be erased." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:665 -msgid "Delete all your (micro-)blog comments?" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:669 -msgid "" -"If you confirm this dialog, all the public and private comments you made " -"on other people's posts will be erased." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:689 -msgid "blog posts and comments" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:694 -msgid "blog posts" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:699 -msgid "comments" -msgstr "" - -#: sat/plugins/plugin_misc_account.py:705 -#, fuzzy -msgid "Deletion confirmation" -msgstr "désinscription confirmée pour [%s]" - -#: sat/plugins/plugin_misc_account.py:707 -#, python-format -msgid "Your %(target)s have been deleted." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:709 -msgid "" -"Known issue of the demo version: you need to refresh the page to make the" -" deleted posts actually disappear." -msgstr "" - -#: sat/plugins/plugin_misc_account.py:718 -#, python-format -msgid "Your %(target)s could not be deleted: %(message)s" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:50 -msgid "Manage Android platform specificities, like pause or notifications" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:92 -msgid "sound on notifications" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:94 -#, fuzzy -msgid "Normal" -msgstr "Général" - -#: sat/plugins/plugin_misc_android.py:95 sat/plugins/plugin_misc_android.py:103 -msgid "Never" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:99 -msgid "Vibrate on notifications" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:101 -#, fuzzy -msgid "Always" -msgstr "Chercher les transports" - -#: sat/plugins/plugin_misc_android.py:102 -msgid "In vibrate mode" -msgstr "" - -#: sat/plugins/plugin_misc_android.py:243 -#, fuzzy -msgid "plugin Android initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_android.py:362 -#, fuzzy -msgid "new message from {contact}" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat/plugins/plugin_misc_app_manager.py:64 -msgid "" -"Applications Manager\n" -"\n" -"Manage external applications using packagers, OS " -"virtualization/containers or other\n" -"software management tools.\n" -msgstr "" - -#: sat/plugins/plugin_misc_app_manager.py:80 -#, fuzzy -msgid "plugin Applications Manager initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_app_manager.py:166 -msgid "" -"No value found for \"public_url\", using \"example.org\" for now, please " -"set the proper value in libervia.conf" -msgstr "" - -#: sat/plugins/plugin_misc_app_manager.py:170 -msgid "" -"invalid value for \"public_url\" ({value}), it musts not start with " -"schema (\"http\"), ignoring it and using \"example.org\" instead" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:43 -msgid "Attachments handler" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:53 -#, fuzzy -msgid "plugin Attach initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_attach.py:109 -msgid "Can't resize attachment of unknown type: {attachment}" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:125 -msgid "Attachment {path!r} has been resized at {new_path!r}" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:129 -msgid "Can't resize attachment of type {main_type!r}: {attachment}" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:143 -msgid "No plugin can handle attachment with {destinee}" -msgstr "" - -#: sat/plugins/plugin_misc_attach.py:210 -msgid "certificate check disabled for upload, this is dangerous!" -msgstr "" - -#: sat/plugins/plugin_misc_debug.py:35 -msgid "Set of method to make development and debugging easier" -msgstr "" - -#: sat/plugins/plugin_misc_debug.py:41 -#, fuzzy -msgid "Plugin Debug initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_download.py:43 -msgid "File download management" -msgstr "" - -#: sat/plugins/plugin_misc_download.py:50 -#, fuzzy -msgid "plugin Download initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_download.py:95 -msgid "Can't download file: {reason}" -msgstr "" - -#: sat/plugins/plugin_misc_download.py:99 sat_frontends/jp/cmd_file.py:498 -#, fuzzy -msgid "Can't download file" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat/plugins/plugin_misc_download.py:176 -msgid "certificate check disabled for download, this is dangerous!" -msgstr "" - -#: sat/plugins/plugin_misc_download.py:187 -msgid "Can't download URI {uri}: {reason}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:44 -msgid "invitation of people without XMPP account" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:59 -msgid "You have been invited by {host_name} to {app_name}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:60 -msgid "" -"Hello {name}!\n" -"\n" -"You have received an invitation from {host_name} to participate to " -"\"{app_name}\".\n" -"To join, you just have to click on the following URL:\n" -"{url}\n" -"\n" -"Please note that this URL should not be shared with anybody!\n" -"If you want more details on {app_name}, you can check {app_url}.\n" -"\n" -"Welcome!\n" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:76 -#, fuzzy -msgid "plugin Invitations initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_email_invitation.py:105 -msgid "You can't use following key(s) in extra, they are reserved: {}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:198 -msgid "You can't use following key(s) in both args and extra: {}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:207 -msgid "You need to provide a main email address before using emails_extra" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:213 -msgid "You need to provide url_template if you use default message body" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:216 -#, fuzzy -msgid "creating an invitation" -msgstr "Connexion..." - -#: sat/plugins/plugin_misc_email_invitation.py:237 -msgid "You need to specify xmpp_domain in sat.conf" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:251 -msgid "Can't create XMPP account" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:254 -msgid "requested jid already exists, trying with {}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:265 -msgid "account {jid_} created" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:317 -msgid "somebody" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:345 -msgid "Not all arguments have been consumed: {}" -msgstr "" - -#: sat/plugins/plugin_misc_email_invitation.py:443 -msgid "Skipping reserved key {key}" -msgstr "" - -#: sat/plugins/plugin_misc_extra_pep.py:38 -msgid "Display messages from extra PEP services" -msgstr "" - -#: sat/plugins/plugin_misc_extra_pep.py:69 -#, fuzzy -msgid "Plugin Extra PEP initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_file.py:45 -msgid "" -"File Tansfer Management:\n" -"This plugin manage the various ways of sending a file, and choose the " -"best one." -msgstr "" - -#: sat/plugins/plugin_misc_file.py:52 -msgid "Please select a file to send to {peer}" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:53 -msgid "File sending" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:54 -msgid "" -"{peer} wants to send the file \"{name}\" to you:\n" -"{desc}\n" -"\n" -"The file has a size of {size_human}\n" -"\n" -"Do you accept ?" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:58 -#, fuzzy -msgid "Confirm file transfer" -msgstr "Transfert de fichier" - -#: sat/plugins/plugin_misc_file.py:59 -msgid "File {} already exists, are you sure you want to overwrite ?" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:60 -#, fuzzy -msgid "File exists" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_misc_file.py:70 -#, fuzzy -msgid "plugin File initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_file.py:82 -#, fuzzy -msgid "Action" -msgstr "Connexion..." - -#: sat/plugins/plugin_misc_file.py:82 -#, fuzzy -msgid "send file" -msgstr "Envoi un fichier" - -#: sat/plugins/plugin_misc_file.py:85 -#, fuzzy -msgid "Send a file" -msgstr "Envoi un fichier" - -#: sat/plugins/plugin_misc_file.py:121 -msgid "{name} method will be used to send the file" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:132 -msgid "Can't send {filepath} to {peer_jid} with {method_name}: {reason}" -msgstr "" - -#: sat/plugins/plugin_misc_file.py:166 sat/plugins/plugin_xep_0100.py:101 -msgid "Invalid JID" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:36 -#, fuzzy -msgid "forums management" -msgstr "Initialisation du gestionnaire de mémoire" - -#: sat/plugins/plugin_misc_forums.py:43 -msgid "forums management plugin" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:54 -#, fuzzy -msgid "forums plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_forums.py:97 -msgid "forums arguments must be a list of forums" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:109 -msgid "A forum item must be a dictionary" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:114 -msgid "following forum name is not unique: {name}" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:116 -msgid "creating missing forum node" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:130 -msgid "Unknown forum attribute: {key}" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:136 -msgid "forum need a title or a name" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:138 -msgid "forum need uri or sub-forums" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:154 -msgid "missing element" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:160 -msgid "Unexpected element: {xml}" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:168 -msgid "Following attributes are unknown: {unknown}" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:176 -msgid "invalid forum, ignoring: {xml}" -msgstr "" - -#: sat/plugins/plugin_misc_forums.py:180 -msgid "unkown forums sub element: {xml}" -msgstr "" - -#: sat/plugins/plugin_misc_groupblog.py:53 -#, fuzzy -msgid "Implementation of microblogging fine permissions" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_misc_groupblog.py:61 -#, fuzzy -msgid "Group blog plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_misc_groupblog.py:80 -msgid "Server is not able to manage item-access pubsub, we can't use group blog" -msgstr "" - -#: sat/plugins/plugin_misc_groupblog.py:86 -msgid "Server can manage group blogs" -msgstr "" - -#: sat/plugins/plugin_misc_identity.py:49 -msgid "Identity manager" -msgstr "" - -#: sat/plugins/plugin_misc_identity.py:58 -#, fuzzy -msgid "Plugin Identity initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_identity.py:293 -#: sat/plugins/plugin_misc_identity.py:365 -msgid "No callback registered for {metadata_name}" -msgstr "" - -#: sat/plugins/plugin_misc_identity.py:316 -msgid "Error while trying to get {metadata_name} with {callback}: {e}" -msgstr "" - -#: sat/plugins/plugin_misc_identity.py:376 -msgid "Error while trying to set {metadata_name} with {callback}: {e}" -msgstr "" - -#: sat/plugins/plugin_misc_identity.py:691 -msgid "Can't set metadata {metadata_name!r}: {reason}" -msgstr "" - -#: sat/plugins/plugin_misc_ip.py:57 -msgid "This plugin help to discover our external IP address." -msgstr "" - -#: sat/plugins/plugin_misc_ip.py:64 -msgid "Allow external get IP" -msgstr "" - -#: sat/plugins/plugin_misc_ip.py:67 -msgid "Confirm external site request" -msgstr "" - -#: sat/plugins/plugin_misc_ip.py:68 -msgid "" -"To facilitate data transfer, we need to contact a website.\n" -"A request will be done on {page}\n" -"That means that administrators of {domain} can know that you use " -"\"{app_name}\" and your IP Address.\n" -"\n" -"IP address is an identifier to locate you on Internet (similar to a phone" -" number).\n" -"\n" -"Do you agree to do this request ?\n" -msgstr "" - -#: sat/plugins/plugin_misc_ip.py:100 -#, fuzzy -msgid "plugin IP discovery initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_lists.py:39 -msgid "Pubsub Lists" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:47 -msgid "Pubsub lists management plugin" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:52 -msgid "TODO List" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:63 sat/plugins/plugin_misc_lists.py:113 -#: sat/plugins/plugin_misc_lists.py:156 -msgid "status" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:67 -msgid "to do" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:71 -#, fuzzy -msgid "in progress" -msgstr "Progression: " - -#: sat/plugins/plugin_misc_lists.py:75 -#, fuzzy -msgid "done" -msgstr "En ligne" - -#: sat/plugins/plugin_misc_lists.py:83 sat/plugins/plugin_misc_lists.py:180 -msgid "priority" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:87 sat/plugins/plugin_misc_lists.py:184 -msgid "major" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:91 sat/plugins/plugin_misc_lists.py:188 -#, fuzzy -msgid "normal" -msgstr "Général" - -#: sat/plugins/plugin_misc_lists.py:95 sat/plugins/plugin_misc_lists.py:192 -msgid "minor" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:106 -msgid "Grocery List" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:109 sat_frontends/jp/cmd_info.py:69 -#: sat_frontends/jp/cmd_info.py:111 -#, fuzzy -msgid "name" -msgstr "Jeu" - -#: sat/plugins/plugin_misc_lists.py:110 -msgid "quantity" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:117 -msgid "to buy" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:121 -#, fuzzy -msgid "bought" -msgstr "À propos" - -#: sat/plugins/plugin_misc_lists.py:130 -msgid "Tickets" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:140 sat_frontends/jp/cmd_info.py:69 -msgid "type" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:144 -msgid "bug" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:148 -#, fuzzy -msgid "feature request" -msgstr "Gestion des paramètres" - -#: sat/plugins/plugin_misc_lists.py:160 -#, fuzzy -msgid "queued" -msgstr "refusé" - -#: sat/plugins/plugin_misc_lists.py:164 -msgid "started" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:168 -msgid "review" -msgstr "" - -#: sat/plugins/plugin_misc_lists.py:172 -#, fuzzy -msgid "closed" -msgstr "fermeture" - -#: sat/plugins/plugin_misc_lists.py:208 -#, fuzzy -msgid "Pubsub lists plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_merge_requests.py:35 -msgid "Merge requests management" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:42 -msgid "Merge requests management plugin" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:69 -#, fuzzy -msgid "Merge requests plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_misc_merge_requests.py:121 -#, fuzzy -msgid "a handler with name {name} already exists!" -msgstr "Ce nom de profile existe déjà" - -#: sat/plugins/plugin_misc_merge_requests.py:134 -msgid "" -"merge requests of type {type} are already handled by {old_handler}, " -"ignoring {new_handler}" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:241 -msgid "repository must be specified" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:244 -msgid "{field} is set by backend, you must not set it in frontend" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:253 -msgid "{name} handler will be used" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:256 -msgid "repository {path} can't be handled by any installed handler" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:259 -msgid "no handler for this repository has been found" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:265 -msgid "No handler of this name found" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:269 -msgid "export data is empty, do you have any change to send?" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:312 -msgid "No handler can handle data type \"{type}\"" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:348 -msgid "No handler found to import {data_type}" -msgstr "" - -#: sat/plugins/plugin_misc_merge_requests.py:350 -msgid "Importing patch [{item_id}] using {name} handler" -msgstr "" - -#: sat/plugins/plugin_misc_nat_port.py:45 -msgid "Automatic NAT port mapping using UPnP" -msgstr "" - -#: sat/plugins/plugin_misc_nat_port.py:62 -#, fuzzy -msgid "plugin NAT Port initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_nat_port.py:177 -msgid "addportmapping error: {msg}" -msgstr "" - -#: sat/plugins/plugin_misc_nat_port.py:215 -msgid "error while trying to map ports: {msg}" -msgstr "" - -#: sat/plugins/plugin_misc_quiz.py:42 -#, fuzzy -msgid "Implementation of Quiz game" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_misc_quiz.py:55 -#, fuzzy -msgid "Plugin Quiz initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_quiz.py:345 -msgid "" -"Bienvenue dans cette partie rapide de quizz, le premier à atteindre le " -"score de 9 remporte le jeu\n" -"\n" -"Attention, tu es prêt ?" -msgstr "" - -#: sat/plugins/plugin_misc_quiz.py:380 sat/plugins/plugin_misc_tarot.py:664 -#, 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]" - -#: sat/plugins/plugin_misc_quiz.py:456 sat/plugins/plugin_misc_radiocol.py:353 -#, fuzzy, python-format -msgid "Unmanaged game element: %s" -msgstr "élément de jeu de carte inconnu: %s" - -#: sat/plugins/plugin_misc_radiocol.py:57 -#, fuzzy -msgid "Implementation of radio collective" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_misc_radiocol.py:76 -#, fuzzy -msgid "Radio collective initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_radiocol.py:180 -msgid "" -"The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are " -"accepted." -msgstr "" - -#: sat/plugins/plugin_misc_radiocol.py:210 -msgid "No more participants in the radiocol: cleaning data" -msgstr "" - -#: sat/plugins/plugin_misc_radiocol.py:249 -msgid "INTERNAL ERROR: can't find full path of the song to delete" -msgstr "" - -#: sat/plugins/plugin_misc_radiocol.py:258 -#, python-format -msgid "INTERNAL ERROR: can't find %s on the file system" -msgstr "" - -#: sat/plugins/plugin_misc_register_account.py:41 -#, fuzzy -msgid "Register XMPP account" -msgstr "Enregistrement d'un nouveau compte" - -#: sat/plugins/plugin_misc_register_account.py:49 -#, fuzzy -msgid "Plugin Register Account initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_register_account.py:76 -msgid "Missing values" -msgstr "" - -#: sat/plugins/plugin_misc_register_account.py:78 -#, fuzzy -msgid "No user JID or password 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/plugins/plugin_misc_register_account.py:87 -msgid "Register new account" -msgstr "Enregistrement d'un nouveau compte" - -#: sat/plugins/plugin_misc_register_account.py:92 -msgid "Do you want to register a new XMPP account {jid}?" -msgstr "" - -#: sat/plugins/plugin_misc_register_account.py:134 -#, fuzzy -msgid "Registration successful." -msgstr "Inscription réussie" - -#: sat/plugins/plugin_misc_register_account.py:138 -msgid "Failure" -msgstr "" - -#: sat/plugins/plugin_misc_register_account.py:139 -#, fuzzy, python-format -msgid "Registration failed: %s" -msgstr "Échec de l'inscription: %s" - -#: sat/plugins/plugin_misc_register_account.py:143 -#, fuzzy -msgid "Username already exists, please choose an other one." -msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" - -#: sat/plugins/plugin_misc_room_game.py:49 -msgid "Base class for MUC games" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:221 -#, python-format -msgid "%(user)s not allowed to join the game %(game)s in %(room)s" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:380 -#, python-format -msgid "%(user)s not allowed to invite for the game %(game)s in %(room)s" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:433 -#, python-format -msgid "Still waiting for %(users)s before starting the game %(game)s in %(room)s" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:472 -#, python-format -msgid "Preparing room for %s game" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:475 -#, fuzzy -msgid "Unknown profile" -msgstr "Afficher profile" - -#: sat/plugins/plugin_misc_room_game.py:583 -#, fuzzy, python-format -msgid "%(game)s game already created in room %(room)s" -msgstr "%(profile)s est déjà dans le salon %(room_jid)s" - -#: sat/plugins/plugin_misc_room_game.py:589 -#, python-format -msgid "%(game)s game in room %(room)s can only be created by %(user)s" -msgstr "" - -#: sat/plugins/plugin_misc_room_game.py:610 -#, fuzzy, python-format -msgid "Creating %(game)s game in room %(room)s" -msgstr "Construction du jeu de Tarot" - -#: sat/plugins/plugin_misc_room_game.py:615 -#: sat/plugins/plugin_misc_room_game.py:646 -#: sat/plugins/plugin_misc_tarot.py:581 -#, python-format -msgid "profile %s is unknown" -msgstr "le profil %s est inconnu" - -#: sat/plugins/plugin_misc_room_game.py:661 -#, python-format -msgid "new round for %s game" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:44 -msgid "Plugin for static blogs" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:66 -#, fuzzy -msgid "Page title" -msgstr "Petite" - -#: sat/plugins/plugin_misc_static_blog.py:68 -msgid "Banner URL" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:70 -msgid "Background image URL" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:72 -msgid "Keywords" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:74 -msgid "Description" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:97 sat/plugins/plugin_sec_otr.py:508 -#: sat/plugins/plugin_sec_otr.py:542 sat/plugins/plugin_sec_otr.py:568 -#: sat/plugins/plugin_sec_otr.py:592 -msgid "jid key is not present !" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:102 -msgid "Not available" -msgstr "" - -#: sat/plugins/plugin_misc_static_blog.py:104 -msgid "Retrieving a blog from an external domain is not implemented yet." -msgstr "" - -#: sat/plugins/plugin_misc_tarot.py:47 -#, fuzzy -msgid "Implementation of Tarot card game" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_misc_tarot.py:60 -#, fuzzy -msgid "Plugin Tarot initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_tarot.py:78 -msgid "Passe" -msgstr "Passe" - -#: sat/plugins/plugin_misc_tarot.py:79 -msgid "Petite" -msgstr "Petite" - -#: sat/plugins/plugin_misc_tarot.py:80 -msgid "Garde" -msgstr "Garde" - -#: sat/plugins/plugin_misc_tarot.py:81 -msgid "Garde Sans" -msgstr "Garde Sans" - -#: sat/plugins/plugin_misc_tarot.py:82 -msgid "Garde Contre" -msgstr "Garde Contre" - -#: sat/plugins/plugin_misc_tarot.py:171 -msgid "contrat selection" -msgstr "Sélection du contrat" - -#: sat/plugins/plugin_misc_tarot.py:189 -msgid "scores" -msgstr "points" - -#: sat/plugins/plugin_misc_tarot.py:273 sat/plugins/plugin_misc_tarot.py:313 -#, 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" - -#: sat/plugins/plugin_misc_tarot.py:327 -#, 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" - -#: sat/plugins/plugin_misc_tarot.py:338 -#: sat_frontends/primitivus/game_tarot.py:309 -msgid "Draw game" -msgstr "" - -#: sat/plugins/plugin_misc_tarot.py:341 sat/plugins/plugin_misc_tarot.py:436 -#, 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" - -#: sat/plugins/plugin_misc_tarot.py:397 -#, fuzzy -msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" -msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" - -#: sat/plugins/plugin_misc_tarot.py:422 -#, fuzzy, 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): " -"(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" - -#: sat/plugins/plugin_misc_tarot.py:507 -msgid "Internal error: unmanaged game stage" -msgstr "ERREUR INTERNE: état de jeu inconnu" - -#: sat/plugins/plugin_misc_tarot.py:530 sat/plugins/plugin_misc_tarot.py:562 -msgid "session id doesn't exist, session has probably expired" -msgstr "" - -#: sat/plugins/plugin_misc_tarot.py:540 -#, python-format -msgid "contrat [%(contrat)s] choosed by %(profile)s" -msgstr "contrat [%(contrat)s] choisi par %(profile)s" - -#: sat/plugins/plugin_misc_tarot.py:584 -#, fuzzy, python-format -msgid "Cards played by %(profile)s: [%(cards)s]" -msgstr "Cartes jouées par %(profile)s: [%(cards)s]" - -#: sat/plugins/plugin_misc_tarot.py:709 -msgid "Everybody is passing, round ended" -msgstr "" - -#: sat/plugins/plugin_misc_tarot.py:723 -#, python-format -msgid "%(player)s win the bid with %(contrat)s" -msgstr "%(player)s remporte l'enchère avec %(contrat)s" - -#: sat/plugins/plugin_misc_tarot.py:751 -#, fuzzy -msgid "tarot: chien received" -msgstr "tarot: chien reçu" - -#: sat/plugins/plugin_misc_tarot.py:828 -#, python-format -msgid "The winner of this trick is %s" -msgstr "le vainqueur de cette main est %s" - -#: sat/plugins/plugin_misc_tarot.py:896 -#, fuzzy, python-format -msgid "Unmanaged error type: %s" -msgstr "type d'erreur inconnu: %s" - -#: sat/plugins/plugin_misc_tarot.py:898 -#, python-format -msgid "Unmanaged card game element: %s" -msgstr "élément de jeu de carte inconnu: %s" - -#: sat/plugins/plugin_misc_text_commands.py:40 -msgid "IRC like text commands" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:60 -msgid "" -"Type '/help' to get a list of the available commands. If you didn't want " -"to use a command, please start your message with '//' to escape the " -"slash." -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:66 -#, fuzzy -msgid "Text commands initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_misc_text_commands.py:162 -#, python-format -msgid "Skipping not callable [%s] attribute" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:166 -msgid "Skipping cmd_ method" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:173 -msgid "Conflict for command [{old_name}], renaming it to [{new_name}]" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:180 -#, python-format -msgid "Registered text command [%s]" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:244 -#, fuzzy, python-format -msgid "Invalid command /%s. " -msgstr "Mauvais nom de profile" - -#: sat/plugins/plugin_misc_text_commands.py:277 -#, fuzzy, python-format -msgid "Unknown command /%s. " -msgstr "Type d'action inconnu" - -#: sat/plugins/plugin_misc_text_commands.py:286 -msgid "group discussions" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:288 -msgid "one to one discussions" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:290 -msgid "/{command} command only applies in {context}." -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:374 -msgid "Invalid jid, can't whois" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:380 -#, python-format -msgid "whois for %(jid)s" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:436 -msgid "Invalid command name [{}]\n" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:457 -#, python-format -msgid "" -"Text commands available:\n" -"%s" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:462 -msgid "" -"/{name}: {short_help}\n" -"{syntax}{args_help}" -msgstr "" - -#: sat/plugins/plugin_misc_text_commands.py:465 -msgid " syntax: {}\n" -msgstr "" - -#: sat/plugins/plugin_misc_text_syntaxes.py:43 sat/test/constants.py:56 -#, fuzzy -msgid "Composition" -msgstr "Connexion..." - -#: sat/plugins/plugin_misc_text_syntaxes.py:142 -msgid "Management of various text syntaxes (XHTML-IM, Markdown, etc)" -msgstr "" - -#: sat/plugins/plugin_misc_text_syntaxes.py:184 -#, fuzzy -msgid "Text syntaxes plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_misc_upload.py:41 -msgid "File upload management" -msgstr "" - -#: sat/plugins/plugin_misc_upload.py:45 -#, fuzzy -msgid "Please select a file to upload" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat/plugins/plugin_misc_upload.py:46 -msgid "File upload" -msgstr "" - -#: sat/plugins/plugin_misc_upload.py:53 -#, fuzzy -msgid "plugin Upload initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_upload.py:92 -msgid "Can't upload file: {reason}" -msgstr "" - -#: sat/plugins/plugin_misc_upload.py:96 sat_frontends/jp/cmd_file.py:586 -msgid "Can't upload file" -msgstr "" - -#: sat/plugins/plugin_misc_uri_finder.py:32 -msgid "URI finder" -msgstr "" - -#: sat/plugins/plugin_misc_uri_finder.py:39 -msgid "" -" Plugin to find URIs in well know location.\n" -" This allows to retrieve settings to work with a project (e.g. pubsub " -"node used for merge-requests).\n" -" " -msgstr "" - -#: sat/plugins/plugin_misc_uri_finder.py:52 -#, fuzzy -msgid "URI finder plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_uri_finder.py:87 -msgid "Ignoring already found uri for key \"{key}\"" -msgstr "" - -#: sat/plugins/plugin_misc_watched.py:37 -msgid "Watch for entities presence, and send notification accordingly" -msgstr "" - -#: sat/plugins/plugin_misc_watched.py:45 -#, fuzzy, python-format -msgid "Watched entity {entity} is connected" -msgstr "Vous êtes déjà connecté !" - -#: sat/plugins/plugin_misc_watched.py:62 -#, fuzzy -msgid "Watched initialisation" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_misc_welcome.py:34 -msgid "Plugin which manage welcome message and things to to on first connection." -msgstr "" - -#: sat/plugins/plugin_misc_welcome.py:42 -msgid "Display welcome message" -msgstr "" - -#: sat/plugins/plugin_misc_welcome.py:43 -msgid "Welcome to Libervia/Salut à Toi" -msgstr "" - -#: sat/plugins/plugin_misc_welcome.py:46 -msgid "" -"Welcome to a free (as in freedom) network!\n" -"\n" -"If you have any trouble, or you want to help us for the bug hunting, you " -"can contact us in real time chat by using the “Help / Official chat room”" -" menu.\n" -"\n" -"To use Libervia, you'll need to add contacts, either people you know, or " -"people you discover by using the “Contacts / Search directory” menu.\n" -"\n" -"We hope that you'll enjoy using this project.\n" -"\n" -"The Libervia/Salut à Toi Team\n" -msgstr "" - -#: sat/plugins/plugin_misc_welcome.py:75 -#, fuzzy -msgid "plugin Welcome initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_xmllog.py:36 -msgid "Send raw XML logs to bridge" -msgstr "" - -#: sat/plugins/plugin_misc_xmllog.py:51 -#, fuzzy -msgid "Activate XML log" -msgstr "Lancement du flux" - -#: sat/plugins/plugin_misc_xmllog.py:55 -#, fuzzy -msgid "Plugin XML Log initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_misc_xmllog.py:69 -msgid "XML log activated" -msgstr "" - -#: sat/plugins/plugin_misc_xmllog.py:81 -#, fuzzy -msgid "INTERNAL ERROR: Unmanaged XML type" -msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" - -#: sat/plugins/plugin_sec_aesgcm.py:48 -msgid "" -" Implementation of AES-GCM scheme, a way to encrypt files (not " -"official XMPP standard).\n" -" See https://xmpp.org/extensions/inbox/omemo-media-sharing.html for " -"details\n" -" " -msgstr "" - -#: sat/plugins/plugin_sec_aesgcm.py:63 -#, fuzzy -msgid "AESGCM plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_sec_otr.py:50 -#, fuzzy -msgid "Implementation of OTR" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_sec_otr.py:55 -msgid "OTR" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:56 -msgid "" -"To authenticate your correspondent, you need to give your below " -"fingerprint *BY AN EXTERNAL CANAL* (i.e. not in this chat), and check " -"that the one he gives you is the same as below. If there is a mismatch, " -"there can be a spy between you!" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:61 -msgid "" -"You private key is used to encrypt messages for your correspondent, " -"nobody except you must know it, if you are in doubt, you should drop it!" -"\n" -"\n" -"Are you sure you want to drop your private key?" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:67 -msgid "Some of advanced features are disabled !" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:169 -#, python-format -msgid "/!\\ conversation with %(other_jid)s is now UNENCRYPTED" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:182 -#, fuzzy -msgid "trusted" -msgstr "refusé" - -#: sat/plugins/plugin_sec_otr.py:182 -#, fuzzy -msgid "untrusted" -msgstr "refusé" - -#: sat/plugins/plugin_sec_otr.py:185 -msgid "{trusted} OTR conversation with {other_jid} REFRESHED" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:189 -msgid "" -"{trusted} encrypted OTR conversation started with {other_jid}\n" -"{extra_info}" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:201 -msgid "OTR conversation with {other_jid} is FINISHED" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:209 -msgid "Unknown OTR state" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:249 -msgid "Save is called but privkey is None !" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:308 -#, fuzzy -msgid "OTR plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_sec_otr.py:418 -msgid "You have no private key yet, start an OTR conversation to have one" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:424 -msgid "No private key" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:436 -msgid "" -"Your fingerprint is:\n" -"{fingerprint}\n" -"\n" -"Start an OTR conversation to have your correspondent one." -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:442 sat/plugins/plugin_xep_0384.py:687 -msgid "Fingerprint" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:453 -msgid "Your correspondent {correspondent} is now TRUSTED" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:459 -msgid "Your correspondent {correspondent} is now UNTRUSTED" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:477 -msgid "Authentication ({entity_jid})" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:483 -msgid "" -"Your own fingerprint is:\n" -"{fingerprint}" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:486 -msgid "" -"Your correspondent fingerprint should be:\n" -"{fingerprint}" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:492 -msgid "Is your correspondent fingerprint the same as here ?" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:494 -msgid "yes" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:494 -msgid "no" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:520 -msgid "" -"Can't start an OTR session, there is already an encrypted session with " -"{name}" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:598 -msgid "You don't have a private key yet !" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:611 -msgid "Your private key has been dropped" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:620 -msgid "Confirm private key drop" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:649 -msgid "WARNING: received unencrypted data in a supposedly encrypted context" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:656 -msgid "WARNING: received OTR encrypted data in an unencrypted context" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:662 -msgid "WARNING: received OTR error message: {msg}" -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:668 -#, fuzzy, python-format -msgid "Error while trying de decrypt OTR message: {msg}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat/plugins/plugin_sec_otr.py:780 -msgid "" -"Your message was not sent because your correspondent closed the encrypted" -" conversation on his/her side. Either close your own side, or refresh the" -" session." -msgstr "" - -#: sat/plugins/plugin_sec_otr.py:785 -msgid "Message discarded because closed encryption channel" -msgstr "" - -#: sat/plugins/plugin_syntax_wiki_dotclear.py:40 -#, fuzzy -msgid "Implementation of Dotclear wiki syntax" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_syntax_wiki_dotclear.py:664 -#, fuzzy -msgid "Dotclear wiki syntax plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_tickets_import.py:38 -msgid "" -"Tickets import management:\n" -"This plugin manage the different tickets importers which can register to " -"it, and handle generic importing tasks." -msgstr "" - -#: sat/plugins/plugin_tickets_import.py:57 -#, fuzzy -msgid "plugin Tickets Import initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_tickets_import.py:111 -msgid "comments_uri key will be generated and must not be used by importer" -msgstr "" - -#: sat/plugins/plugin_tickets_import.py:115 -msgid "{key} must be a list" -msgstr "" - -#: sat/plugins/plugin_tickets_import.py:174 -msgid "mapping option must be a dictionary" -msgstr "" - -#: sat/plugins/plugin_tickets_import.py:179 -msgid "keys and values of mapping must be sources and destinations ticket fields" -msgstr "" - -#: sat/plugins/plugin_tickets_import_bugzilla.py:41 -msgid "Tickets importer for Bugzilla" -msgstr "" - -#: sat/plugins/plugin_tickets_import_bugzilla.py:44 -msgid "import tickets from Bugzilla xml export file" -msgstr "" - -#: sat/plugins/plugin_tickets_import_bugzilla.py:46 -msgid "" -"This importer handle Bugzilla xml export file.\n" -"\n" -"To use it, you'll need to export tickets using XML.\n" -"Tickets will be uploaded with the same ID as for Bugzilla, any existing " -"ticket with this ID will be replaced.\n" -"\n" -"location: you must use the absolute path to your .xml file\n" -msgstr "" - -#: sat/plugins/plugin_tickets_import_bugzilla.py:128 -#, fuzzy -msgid "Bugilla Import plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_tmp_directory_subscription.py:37 -#, fuzzy -msgid "Implementation of directory subscription" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_tmp_directory_subscription.py:47 -#, fuzzy -msgid "Directory subscription plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_tmp_directory_subscription.py:50 -#: sat/plugins/plugin_xep_0050.py:315 sat/plugins/plugin_xep_0100.py:84 -msgid "Service" -msgstr "" - -#: sat/plugins/plugin_tmp_directory_subscription.py:50 -msgid "Directory subscription" -msgstr "" - -#: sat/plugins/plugin_tmp_directory_subscription.py:53 -msgid "User directory subscription" -msgstr "" - -#: sat/plugins/plugin_xep_0020.py:46 -#, fuzzy -msgid "Implementation of Feature Negotiation" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0020.py:52 -#, fuzzy -msgid "Plugin XEP_0020 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0020.py:104 -msgid "More than one value choosed for {}, keeping the first one" -msgstr "" - -#: sat/plugins/plugin_xep_0033.py:66 -#, fuzzy -msgid "Implementation of Extended Stanza Addressing" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0033.py:76 -#, fuzzy -msgid "Extended Stanza Addressing plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0033.py:97 -msgid "XEP-0033 is being used but the server doesn't support it!" -msgstr "" - -#: sat/plugins/plugin_xep_0033.py:103 -#, fuzzy -msgid " or " -msgstr "Formulaire" - -#: sat/plugins/plugin_xep_0033.py:105 -#, python-format -msgid "" -"Stanzas using XEP-0033 should be addressed to %(expected)s, not " -"%(current)s!" -msgstr "" - -#: sat/plugins/plugin_xep_0033.py:111 -msgid "TODO: addressing has been fixed by the backend... fix it in the frontend!" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:56 -#, fuzzy -msgid "Implementation of Multi-User Chat" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0045.py:89 -#, fuzzy -msgid "Plugin XEP_0045 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0045.py:145 -msgid "MUC" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:145 -#, fuzzy -msgid "configure" -msgstr " Configurer l'application" - -#: sat/plugins/plugin_xep_0045.py:146 -#, fuzzy -msgid "Configure Multi-User Chat room" -msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: sat/plugins/plugin_xep_0045.py:194 -msgid "" -"Received non delayed message in a room before its initialisation: " -"state={state}, msg={msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:216 sat/plugins/plugin_xep_0045.py:224 -#: sat/plugins/plugin_xep_0045.py:880 -msgid "This room has not been joined" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:283 -msgid "Room joining cancelled by user" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:288 -msgid "Rooms in {}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:303 -msgid "room locked !" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:306 -#, fuzzy -msgid "Error while configuring the room: {failure_}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat/plugins/plugin_xep_0045.py:322 -msgid "Room {} is restricted" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:323 -msgid "This room is restricted, please enter the password" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:332 -#, fuzzy, python-format -msgid "Error while joining the room {room}{suffix}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat/plugins/plugin_xep_0045.py:334 -msgid "Group chat error" -msgstr "Erreur de salon de discussion" - -#: sat/plugins/plugin_xep_0045.py:401 -msgid "room_jid key is not present !" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:406 -msgid "No configuration available for this room" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:440 sat/plugins/plugin_xep_0045.py:442 -msgid "Session ID doesn't exist, session has probably expired." -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:441 -#, fuzzy -msgid "Room configuration failed" -msgstr "confirmation de type Oui/Non demandée" - -#: sat/plugins/plugin_xep_0045.py:447 -#, fuzzy -msgid "Room configuration succeed" -msgstr "confirmation de type Oui/Non demandée" - -#: sat/plugins/plugin_xep_0045.py:448 -msgid "The new settings have been saved." -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:509 -msgid "No MUC service found on main server" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:538 -msgid "" -"Invalid room identifier: {room_id}'. Please give a room short or full " -"identifier like 'room' or 'room@{muc_service}'." -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:558 -#, fuzzy, python-format -msgid "{profile} is already in room {room_jid}" -msgstr "%(profile)s est déjà dans le salon %(room_jid)s" - -#: sat/plugins/plugin_xep_0045.py:561 -#, fuzzy, python-format -msgid "[{profile}] is joining room {room} with nick {nick}" -msgstr "[%(profile)s] rejoint %(room)s avec %(nick)s" - -#: sat/plugins/plugin_xep_0045.py:729 -msgid "You must provide a member's nick to kick." -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:738 -msgid "You have kicked {}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:740 sat/plugins/plugin_xep_0045.py:776 -msgid " for the following reason: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:763 -msgid "You must provide a valid JID to ban, like in '/ban contact@example.net'" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:774 -msgid "You have banned {}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:802 -msgid "" -"You must provide a valid JID to affiliate, like in '/affiliate " -"contact@example.net member'" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:808 -#, python-format -msgid "You must provide a valid affiliation: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:815 -msgid "New affiliation for {entity}: {affiliation}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:862 -msgid "No known default MUC service {unparsed}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:867 -#, fuzzy, python-format -msgid "{} is not a valid JID!" -msgstr "%s n'est pas un JID valide !" - -#: sat/plugins/plugin_xep_0045.py:885 -#, fuzzy, python-format -msgid "Nickname: %s" -msgstr "fichier enregistré dans %s" - -#: sat/plugins/plugin_xep_0045.py:887 -#, python-format -msgid "Entity: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:889 -#, python-format -msgid "Affiliation: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:891 -#, fuzzy, python-format -msgid "Role: %s" -msgstr "Profile:" - -#: sat/plugins/plugin_xep_0045.py:893 -#, fuzzy, python-format -msgid "Status: %s" -msgstr "Sélection du contrat" - -#: sat/plugins/plugin_xep_0045.py:895 -#, python-format -msgid "Show: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:960 -msgid "" -"room {room} is not in expected state: room is in state {current_state} " -"while we were expecting {expected_state}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1093 -msgid "No message received while offline in {room_jid}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1097 -msgid "We have received {num_mess} message(s) in {room_jid} while offline." -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1141 -msgid "missing nick in presence: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1217 -#, fuzzy, python-format -msgid "user {nick} has joined room {room_id}" -msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" - -#: sat/plugins/plugin_xep_0045.py:1234 -msgid "=> {} has joined the room" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1253 -#, fuzzy, python-format -msgid "Room ({room}) left ({profile})" -msgstr "contrat [%(contrat)s] choisi par %(profile)s" - -#: sat/plugins/plugin_xep_0045.py:1267 -#, fuzzy, python-format -msgid "user {nick} left room {room_id}" -msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" - -#: sat/plugins/plugin_xep_0045.py:1279 -msgid "<= {} has left the room" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1342 -msgid "received history in unexpected state in room {room} (state: {state})" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1350 -msgid "storing the unexpected message anyway, to avoid loss" -msgstr "" - -#: sat/plugins/plugin_xep_0045.py:1437 -#, fuzzy, python-format -msgid "New subject for room ({room_id}): {subject}" -msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" - -#: sat/plugins/plugin_xep_0047.py:62 -#, fuzzy -msgid "Implementation of In-Band Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: sat/plugins/plugin_xep_0047.py:71 -#, fuzzy -msgid "In-Band Bytestreams plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0047.py:162 -msgid "IBB stream opening" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:171 -#, python-format -msgid "Ignoring unexpected IBB transfer: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:176 -msgid "sended jid inconsistency (man in the middle attack attempt ?)" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:206 -msgid "IBB stream closing" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:228 -#, fuzzy -msgid "Received data for an unknown session id" -msgstr "Confirmation inconnue reçue" - -#: sat/plugins/plugin_xep_0047.py:236 -msgid "" -"sended jid inconsistency (man in the middle attack attempt ?)\n" -"initial={initial}\n" -"given={given}" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:246 -msgid "Sequence error" -msgstr "" - -#: sat/plugins/plugin_xep_0047.py:261 -msgid "Invalid base64 data" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:45 -#, fuzzy -msgid "Implementation of bookmarks" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0048.py:58 -#, fuzzy -msgid "Bookmarks plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0048.py:63 sat_frontends/primitivus/base.py:540 -msgid "Groups" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:63 -msgid "Bookmarks" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:66 -msgid "Use and manage bookmarks" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:147 -msgid "Private XML storage not available" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:260 -#, fuzzy -msgid "No room jid selected" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_xep_0048.py:280 -msgid "Bookmarks manager" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:310 sat_frontends/jp/cmd_bookmarks.py:126 -msgid "add a bookmark" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:312 -#, fuzzy -msgid "Name" -msgstr "Jeu" - -#: sat/plugins/plugin_xep_0048.py:314 sat_frontends/jp/cmd_profile.py:175 -msgid "jid" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:316 -msgid "Nickname" -msgstr "Surnon" - -#: sat/plugins/plugin_xep_0048.py:318 -msgid "Autojoin" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:321 sat_frontends/primitivus/xmlui.py:470 -msgid "Save" -msgstr "Sauvegarder" - -#: sat/plugins/plugin_xep_0048.py:367 -msgid "Bookmarks will be local only" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:368 -#, python-format -msgid "Type selected for \"auto\" storage: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:500 -msgid "Bad arguments" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:509 -#, python-format -msgid "All [%s] bookmarks are being removed" -msgstr "" - -#: sat/plugins/plugin_xep_0048.py:520 -msgid "Bookmark added" -msgstr "" - -#: sat/plugins/plugin_xep_0049.py:37 -#, fuzzy -msgid "Implementation of private XML storage" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0049.py:45 -#, fuzzy -msgid "Plugin XEP-0049 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0050.py:51 -#: sat_frontends/quick_frontend/constants.py:29 -msgid "Online" -msgstr "En ligne" - -#: sat/plugins/plugin_xep_0050.py:52 -msgid "Away" -msgstr "Absent" - -#: sat/plugins/plugin_xep_0050.py:53 -#: sat_frontends/quick_frontend/constants.py:30 -msgid "Free for chat" -msgstr "Libre pour discuter" - -#: sat/plugins/plugin_xep_0050.py:54 -#: sat_frontends/quick_frontend/constants.py:32 -msgid "Do not disturb" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:55 -msgid "Left" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:56 sat_frontends/primitivus/base.py:535 -#, fuzzy -msgid "Disconnect" -msgstr "Déconnexion..." - -#: sat/plugins/plugin_xep_0050.py:67 -#, fuzzy -msgid "Implementation of Ad-Hoc Commands" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0050.py:126 -#, fuzzy, python-format -msgid "The groups [{group}] is unknown for profile [{profile}])" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat/plugins/plugin_xep_0050.py:284 -#, fuzzy -msgid "plugin XEP-0050 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0050.py:315 -#, fuzzy -msgid "Commands" -msgstr "Mauvais nom de profile" - -#: sat/plugins/plugin_xep_0050.py:318 -msgid "Execute ad-hoc commands" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:329 -msgid "Status" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:364 -msgid "Missing command element" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:379 -#, fuzzy -msgid "Please select a command" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat/plugins/plugin_xep_0050.py:397 -#, fuzzy, python-format -msgid "Invalid note type [%s], using info" -msgstr "Type d'action inconnu" - -#: sat/plugins/plugin_xep_0050.py:408 -msgid "WARNING" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:409 -#, fuzzy -msgid "ERROR" -msgstr "Erreur" - -#: sat/plugins/plugin_xep_0050.py:457 -msgid "No known payload found in ad-hoc command result, aborting" -msgstr "" - -#: sat/plugins/plugin_xep_0050.py:464 -#, fuzzy -msgid "No payload found" -msgstr "Aucune donnée trouvée" - -#: sat/plugins/plugin_xep_0050.py:574 -#, fuzzy -msgid "Please enter target jid" -msgstr "Veuillez entrer le JID de votre nouveau contact" - -#: sat/plugins/plugin_xep_0050.py:588 -#, fuzzy -msgid "status selection" -msgstr "Sélection du contrat" - -#: sat/plugins/plugin_xep_0050.py:618 -msgid "Status updated" -msgstr "" - -#: sat/plugins/plugin_xep_0054.py:64 -msgid "Implementation of vcard-temp" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0054.py:84 -msgid "Plugin XEP_0054 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0054.py:99 -msgid "No avatar in cache for {profile}" -msgstr "" - -#: sat/plugins/plugin_xep_0054.py:137 -msgid "Decoding binary" -msgstr "Décodage des données" - -#: sat/plugins/plugin_xep_0054.py:242 -msgid "vCard element not found for {entity_jid}: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0054.py:287 -msgid "Can't get vCard for {entity_jid}: {e}" -msgstr "" - -#: sat/plugins/plugin_xep_0054.py:291 -msgid "VCard found" -msgstr "VCard trouvée" - -#: sat/plugins/plugin_xep_0055.py:53 -#, fuzzy -msgid "Implementation of Jabber Search" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0055.py:70 -#, fuzzy -msgid "Jabber search plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0055.py:100 sat/stdui/ui_contact_list.py:39 -#: sat/stdui/ui_contact_list.py:45 sat/stdui/ui_contact_list.py:51 -#: sat_frontends/primitivus/base.py:539 -#: sat_frontends/primitivus/contact_list.py:50 -#, fuzzy -msgid "Contacts" -msgstr "&Contacts" - -#: sat/plugins/plugin_xep_0055.py:100 -msgid "Search directory" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:103 -msgid "Search user directory" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:143 -#, fuzzy, python-format -msgid "Search users" -msgstr "Remplacement de l'utilisateur %s" - -#: sat/plugins/plugin_xep_0055.py:174 -msgid "Search for" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:181 -msgid "Simple search" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:191 sat/plugins/plugin_xep_0055.py:305 -msgid "Search" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:226 -msgid "Advanced search" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:246 -msgid "Search on" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:248 -msgid "Other service" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:256 -msgid "Refresh fields" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:260 -msgid "Displaying the search form for" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:341 -msgid "Search results" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:346 -msgid "The search gave no result" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:386 sat/plugins/plugin_xep_0055.py:493 -msgid "No query element found" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:391 sat/plugins/plugin_xep_0055.py:498 -msgid "No data form found" -msgstr "Aucune donnée trouvée" - -#: sat/plugins/plugin_xep_0055.py:403 -#, fuzzy, python-format -msgid "Fields request failure: %s" -msgstr "Échec de l'inscription: %s" - -#: sat/plugins/plugin_xep_0055.py:478 -msgid "The search could not be performed" -msgstr "" - -#: sat/plugins/plugin_xep_0055.py:510 -#, fuzzy, python-format -msgid "Search request failure: %s" -msgstr "Échec de la désinscription: %s" - -#: sat/plugins/plugin_xep_0059.py:42 -#, fuzzy -msgid "Implementation of Result Set Management" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0059.py:52 -#, fuzzy -msgid "Result Set Management plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0059.py:65 -msgid "rsm_max can't be negative" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:56 -#, fuzzy -msgid "Implementation of PubSub Protocol" -msgstr "Implémentation du protocole de transports" - -#: sat/plugins/plugin_xep_0060.py:95 -#, fuzzy -msgid "PubSub plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0060.py:323 -msgid "Can't retrieve pubsub_service from conf, we'll use first one that we find" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:487 -msgid "Can't parse items: {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:556 -msgid "Invalid item: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:572 -msgid "" -"Can't use publish-options ({options}) on node {node}, re-publishing " -"without them: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:905 sat/plugins/plugin_xep_0060.py:948 -msgid "Invalid result: missing element: {}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:916 sat/plugins/plugin_xep_0060.py:961 -msgid "Invalid result: bad element: {}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:1284 -msgid "Invalid result: missing element: {}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:1289 -msgid "Invalid result: {}" -msgstr "" - -#: sat/plugins/plugin_xep_0060.py:1299 -msgid "Invalid result: bad element: {}" -msgstr "" - -#: sat/plugins/plugin_xep_0065.py:90 -msgid "Implementation of SOCKS5 Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: sat/plugins/plugin_xep_0065.py:528 -msgid "File transfer completed, closing connection" -msgstr "Transfert de fichier terminé, fermeture de la connexion" - -#: sat/plugins/plugin_xep_0065.py:695 -#, python-format -msgid "Socks 5 client connection lost (reason: %s)" -msgstr "Connexion du client SOCKS5 perdue (raison: %s)" - -#: sat/plugins/plugin_xep_0065.py:723 -msgid "Plugin XEP_0065 initialization" -msgstr "Initialisation du plugin XEP_0065" - -#: sat/plugins/plugin_xep_0065.py:781 -#, fuzzy, python-format -msgid "Socks5 Stream server launched on port {}" -msgstr "Lancement du serveur de flux Socks5 sur le port %d" - -#: sat/plugins/plugin_xep_0070.py:56 -#, fuzzy -msgid "Implementation of HTTP Requests via XMPP" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0070.py:66 -#, fuzzy -msgid "Plugin XEP_0070 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0070.py:79 -msgid "XEP-0070 Verifying HTTP Requests via XMPP (iq)" -msgstr "" - -#: sat/plugins/plugin_xep_0070.py:88 -msgid "XEP-0070 Verifying HTTP Requests via XMPP (message)" -msgstr "" - -#: sat/plugins/plugin_xep_0070.py:98 -#, fuzzy -msgid "Auth confirmation" -msgstr "Connexion..." - -#: sat/plugins/plugin_xep_0070.py:99 -msgid "" -"{auth_url} needs to validate your identity, do you agree?\n" -"Validation code : {auth_id}\n" -"\n" -"Please check that this code is the same as on {auth_url}" -msgstr "" - -#: sat/plugins/plugin_xep_0070.py:117 -msgid "XEP-0070 reply iq" -msgstr "" - -#: sat/plugins/plugin_xep_0070.py:122 -msgid "XEP-0070 reply message" -msgstr "" - -#: sat/plugins/plugin_xep_0070.py:127 -msgid "XEP-0070 reply error" -msgstr "" - -#: sat/plugins/plugin_xep_0071.py:55 -#, fuzzy -msgid "Implementation of XHTML-IM" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0071.py:94 -#, fuzzy -msgid "XHTML-IM plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0071.py:223 -msgid "Can't have XHTML and rich content at the same time" -msgstr "" - -#: sat/plugins/plugin_xep_0077.py:41 -msgid "Implementation of in-band registration" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0077.py:54 -#, fuzzy -msgid "Registration asked for {jid}" -msgstr "Éched de l'insciption (%s)" - -#: sat/plugins/plugin_xep_0077.py:79 -msgid "Stream started with {server}, now registering" -msgstr "" - -#: sat/plugins/plugin_xep_0077.py:85 -#, fuzzy, python-format -msgid "Registration answer: {}" -msgstr "réponse à la demande d'inscription: %s" - -#: sat/plugins/plugin_xep_0077.py:89 -#, fuzzy, python-format -msgid "Registration failure: {}" -msgstr "Échec de l'inscription: %s" - -#: sat/plugins/plugin_xep_0077.py:116 -msgid "Plugin XEP_0077 initialization" -msgstr "Initialisation du plugin XEP_0077" - -#: sat/plugins/plugin_xep_0077.py:176 -#, fuzzy -msgid "Can't find data form" -msgstr "Impossible de trouver la VCard de %s" - -#: sat/plugins/plugin_xep_0077.py:178 -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é :(" - -#: sat/plugins/plugin_xep_0077.py:202 sat/plugins/plugin_xep_0077.py:212 -#, python-format -msgid "Registration failure: %s" -msgstr "Échec de l'inscription: %s" - -#: sat/plugins/plugin_xep_0077.py:206 -#, python-format -msgid "registration answer: %s" -msgstr "réponse à la demande d'inscription: %s" - -#: sat/plugins/plugin_xep_0077.py:215 -msgid "Username already exists, please choose an other one" -msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" - -#: sat/plugins/plugin_xep_0077.py:229 -#, fuzzy, python-format -msgid "Asking registration for {}" -msgstr "Demande d'enregistrement pour [%s]" - -#: sat/plugins/plugin_xep_0085.py:55 -#, fuzzy -msgid "Implementation of Chat State Notifications Protocol" -msgstr "Implémentation du protocole de transports" - -#: sat/plugins/plugin_xep_0085.py:97 -msgid "Enable chat state notifications" -msgstr "" - -#: sat/plugins/plugin_xep_0085.py:101 -#, fuzzy -msgid "Chat State Notifications plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0092.py:42 -#, fuzzy -msgid "Implementation of Software Version" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0092.py:48 -#, fuzzy -msgid "Plugin XEP_0092 initialization" -msgstr "Initialisation du plugin XEP_0096" - -#: sat/plugins/plugin_xep_0092.py:119 -#, fuzzy, python-format -msgid "Client name: %s" -msgstr "fichier enregistré dans %s" - -#: sat/plugins/plugin_xep_0092.py:121 -#, python-format -msgid "Client version: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0092.py:123 -#, fuzzy, python-format -msgid "Operating system: %s" -msgstr "réponse à la demande d'inscription: %s" - -#: sat/plugins/plugin_xep_0092.py:128 -msgid "Software version not available" -msgstr "" - -#: sat/plugins/plugin_xep_0092.py:130 -msgid "Client software version request timeout" -msgstr "" - -#: sat/plugins/plugin_xep_0095.py:41 -#, fuzzy -msgid "Implementation of Stream Initiation" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0095.py:54 -#, fuzzy -msgid "Plugin XEP_0095 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0095.py:84 -#, fuzzy -msgid "XEP-0095 Stream initiation" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0095.py:127 -msgid "sending stream initiation accept answer" -msgstr "" - -#: sat/plugins/plugin_xep_0095.py:168 -#, python-format -msgid "Stream Session ID: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0096.py:48 -msgid "Implementation of SI File Transfer" -msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: sat/plugins/plugin_xep_0096.py:55 -#, fuzzy -msgid "Stream Initiation" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0096.py:58 -msgid "Plugin XEP_0096 initialization" -msgstr "Initialisation du plugin XEP_0096" - -#: sat/plugins/plugin_xep_0096.py:129 -msgid "XEP-0096 file transfer requested" -msgstr "" - -#: sat/plugins/plugin_xep_0096.py:377 -#, fuzzy, python-format -msgid "The contact {} has refused your file" -msgstr "Le contact %s a refusé votre inscription" - -#: sat/plugins/plugin_xep_0096.py:378 -#, fuzzy -msgid "File refused" -msgstr "refusé" - -#: sat/plugins/plugin_xep_0096.py:381 -msgid "Error during file transfer" -msgstr "" - -#: sat/plugins/plugin_xep_0096.py:382 -msgid "" -"Something went wrong during the file transfer session initialisation: " -"{reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0096.py:385 -#, fuzzy -msgid "File transfer error" -msgstr "Transfert de fichier" - -#: sat/plugins/plugin_xep_0096.py:394 -#, fuzzy, python-format -msgid "transfer {sid} successfuly finished [{profile}]" -msgstr "Transfert [%s] refusé" - -#: sat/plugins/plugin_xep_0096.py:402 -msgid "transfer {sid} failed [{profile}]: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:37 -msgid "Implementation of Gateways protocol" -msgstr "Implémentation du protocole de transports" - -#: sat/plugins/plugin_xep_0100.py:40 -#, fuzzy -msgid "" -"Be careful ! Gateways allow you to use an external IM (legacy IM), so you" -" can see your contact as XMPP 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, analysed 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)." - -#: sat/plugins/plugin_xep_0100.py:48 -msgid "Internet Relay Chat" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:49 -msgid "XMPP" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:50 -msgid "Tencent QQ" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:51 -msgid "SIP/SIMPLE" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:52 -msgid "ICQ" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:53 -msgid "Yahoo! Messenger" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:54 -msgid "Gadu-Gadu" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:55 -msgid "AOL Instant Messenger" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:56 -msgid "Windows Live Messenger" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:62 -msgid "Gateways plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0100.py:84 -#, fuzzy -msgid "Gateways" -msgstr "Chercher les transports" - -#: sat/plugins/plugin_xep_0100.py:87 -#, fuzzy -msgid "Find gateways" -msgstr "Chercher les transports" - -#: sat/plugins/plugin_xep_0100.py:108 -#, fuzzy, python-format -msgid "Gateways manager (%s)" -msgstr "Gestionnaire de transport" - -#: sat/plugins/plugin_xep_0100.py:121 -#, python-format -msgid "Failed (%s)" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:134 -#, fuzzy -msgid "Use external XMPP server" -msgstr "Utiliser un autre serveur XMPP:" - -#: sat/plugins/plugin_xep_0100.py:136 -msgid "Go !" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:143 -#, fuzzy -msgid "No gateway index selected" -msgstr "Aucun profile sélectionné" - -#: sat/plugins/plugin_xep_0100.py:158 -#, python-format -msgid "" -"INTERNAL ERROR: identity category should always be \"gateway\" in " -"_getTypeString, got \"%s\"" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:166 -msgid "Unknown IM" -msgstr "Messagerie inconnue" - -#: sat/plugins/plugin_xep_0100.py:170 -msgid "Registration successful, doing the rest" -msgstr "Inscription réussie, lancement du reste de la procédure" - -#: sat/plugins/plugin_xep_0100.py:195 -msgid "Timeout" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:211 -#, fuzzy, python-format -msgid "Found gateway [%(jid)s]: %(identity_name)s" -msgstr "Transport trouvé (%(jid)s): %(identity)s" - -#: sat/plugins/plugin_xep_0100.py:222 -#, python-format -msgid "Skipping [%(jid)s] which is not a gateway" -msgstr "" - -#: sat/plugins/plugin_xep_0100.py:231 -msgid "No gateway found" -msgstr "Aucun transport trouvé" - -#: sat/plugins/plugin_xep_0100.py:236 -#, python-format -msgid "item found: %s" -msgstr "object trouvé: %s" - -#: sat/plugins/plugin_xep_0100.py:260 -#, fuzzy, python-format -msgid "find gateways (target = %(target)s, profile = %(profile)s)" -msgstr "transports trouvée (cible = %s)" - -#: sat/plugins/plugin_xep_0106.py:38 -msgid "(Un)escape JID to use disallowed chars in local parts" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:50 -#, fuzzy -msgid "Implementation of entity capabilities" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0115.py:58 -#, fuzzy -msgid "Plugin XEP_0115 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0115.py:73 -msgid "Caps optimisation enabled" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:76 -msgid "Caps optimisation not available" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:154 -#, python-format -msgid "Received invalid capabilities tag: %s" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:170 -msgid "" -"Unknown hash method for entity capabilities: [{hash_method}] (entity: " -"{entity_jid}, node: {node})" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:183 -msgid "" -"Computed hash differ from given hash:\n" -"given: [{given}]\n" -"computed: [{computed}]\n" -"(entity: {entity_jid}, node: {node})" -msgstr "" - -#: sat/plugins/plugin_xep_0115.py:205 -msgid "Couldn't retrieve disco info for {jid}: {error}" -msgstr "" - -#: sat/plugins/plugin_xep_0163.py:42 -#, fuzzy -msgid "Implementation of Personal Eventing Protocol" -msgstr "Implémentation du protocole de transports" - -#: sat/plugins/plugin_xep_0163.py:48 -#, fuzzy -msgid "PEP plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0163.py:131 -#, fuzzy, python-format -msgid "Trying to send personal event with an unknown profile key [%s]" -msgstr "Tentative d'appel d'un profile inconnue" - -#: sat/plugins/plugin_xep_0163.py:136 -#, fuzzy -msgid "Trying to send personal event for an unknown type" -msgstr "Tentative d'assigner un paramètre à un profile inconnu" - -#: sat/plugins/plugin_xep_0163.py:142 -#, fuzzy -msgid "No item found" -msgstr "Aucun transport trouvé" - -#: sat/plugins/plugin_xep_0163.py:149 -msgid "Can't find mood element in mood event" -msgstr "" - -#: sat/plugins/plugin_xep_0163.py:153 -#, fuzzy -msgid "No mood found" -msgstr "Aucune donnée trouvée" - -#: sat/plugins/plugin_xep_0166.py:50 -msgid "{entity} want to start a jingle session with you, do you accept ?" -msgstr "" - -#: sat/plugins/plugin_xep_0166.py:60 -#, fuzzy -msgid "Implementation of Jingle" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0166.py:98 -#, fuzzy -msgid "plugin Jingle initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0166.py:156 -#, fuzzy, python-format -msgid "Error while terminating session: {msg}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat/plugins/plugin_xep_0166.py:395 -msgid "You can't do a jingle session with yourself" -msgstr "" - -#: sat/plugins/plugin_xep_0166.py:511 -msgid "Confirm Jingle session" -msgstr "" - -#: sat/plugins/plugin_xep_0184.py:71 -#, fuzzy -msgid "Implementation of Message Delivery Receipts" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0184.py:96 -msgid "Enable message delivery receipts" -msgstr "" - -#: sat/plugins/plugin_xep_0184.py:100 -#, fuzzy -msgid "Plugin XEP_0184 (message delivery receipts) initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0184.py:136 -msgid "[XEP-0184] Request acknowledgment for message id {}" -msgstr "" - -#: sat/plugins/plugin_xep_0184.py:180 -msgid "[XEP-0184] Receive acknowledgment for message id {}" -msgstr "" - -#: sat/plugins/plugin_xep_0184.py:190 -msgid "[XEP-0184] Delete waiting acknowledgment for message id {}" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:46 -#, fuzzy -msgid "Implementation of Stream Management" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0198.py:134 -#, fuzzy -msgid "Plugin Stream Management initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0198.py:144 -msgid "Invalid ack_timeout value, please check your configuration" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:147 -msgid "Ack timeout disabled" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:149 -msgid "Ack timeout set to {timeout}s" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:171 -msgid "" -"Your server doesn't support stream management ({namespace}), this is used" -" to improve connection problems detection (like network outages). Please " -"ask your server administrator to enable this feature." -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:302 -msgid "" -"Connection failed using location given by server (host: {host}, port: " -"{port}), switching to normal host and port (host: {normal_host}, port: " -"{normal_port})" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:317 -msgid "Incorrect element received, no \"id\" attribute" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:319 -msgid "" -"You're server doesn't support session resuming with stream management, " -"please contact your server administrator to enable it" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:341 -msgid "Invalid location received: {location}" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:359 -msgid "Invalid \"max\" attribute" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:361 -msgid "Using default session max value ({max_s} s)." -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:363 -msgid "Stream Management enabled" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:367 -msgid "Stream Management enabled, with a resumption time of {res_m:.2f} min" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:382 -msgid "" -"Stream session resumed (disconnected for {d_time} s, {count} stanza(s) " -"resent)" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:395 -msgid "Can't use stream management" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:399 -msgid "{msg}: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:408 -msgid "stream resumption not possible, restarting full session" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:495 -msgid "Server returned invalid ack element, disabling stream management: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:501 -msgid "Server acked more stanzas than we have sent, disabling stream management." -msgstr "" - -#: sat/plugins/plugin_xep_0198.py:511 -msgid "Ack was not received in time, aborting connection" -msgstr "" - -#: sat/plugins/plugin_xep_0199.py:39 -#, fuzzy -msgid "Implementation of XMPP Ping" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0199.py:49 -#, fuzzy -msgid "XMPP Ping plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0199.py:109 -msgid "ping error ({err_msg}). Response time: {time} s" -msgstr "" - -#: sat/plugins/plugin_xep_0199.py:123 -msgid "Invalid jid: \"{entity_jid}\"" -msgstr "" - -#: sat/plugins/plugin_xep_0199.py:134 -msgid "XMPP PING received from {from_jid} [{profile}]" -msgstr "" - -#: sat/plugins/plugin_xep_0203.py:45 -#, fuzzy -msgid "Implementation of Delayed Delivery" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0203.py:51 -#, fuzzy -msgid "Delayed Delivery plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0231.py:48 -msgid "Implementation of bits of binary (used for small images/files)" -msgstr "" - -#: sat/plugins/plugin_xep_0231.py:59 -#, fuzzy -msgid "plugin Bits of Binary initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0234.py:54 -#, fuzzy -msgid "Implementation of Jingle File Transfer" -msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: sat/plugins/plugin_xep_0234.py:67 -#, fuzzy -msgid "file transfer" -msgstr "Transfert de fichier" - -#: sat/plugins/plugin_xep_0234.py:70 -#, fuzzy -msgid "plugin Jingle File Transfer initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0234.py:380 -msgid "hash_algo must be set if file_hash is set" -msgstr "" - -#: sat/plugins/plugin_xep_0234.py:385 -msgid "file_hash must be set if hash_algo is set" -msgstr "" - -#: sat/plugins/plugin_xep_0234.py:419 -msgid "only the following keys are allowed in extra: {keys}" -msgstr "" - -#: sat/plugins/plugin_xep_0234.py:454 -msgid "you need to provide at least name or file hash" -msgstr "" - -#: sat/plugins/plugin_xep_0234.py:524 -#, fuzzy -msgid "File continue is not implemented yet" -msgstr "getGame n'est pas implémenté dans ce frontend" - -#: sat/plugins/plugin_xep_0249.py:55 -#, fuzzy -msgid "Implementation of Direct MUC Invitations" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0249.py:75 -msgid "Auto-join MUC on invitation" -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:86 -#, fuzzy -msgid "Plugin XEP_0249 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0249.py:140 -#, python-format -msgid "Invitation accepted for room %(room)s [%(profile)s]" -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:155 -msgid "invalid invitation received: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:159 -#, python-format -msgid "Invitation received for room %(room)s [%(profile)s]" -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:170 -msgid "Invitation silently discarded because user is already in the room." -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:181 -#, python-format -msgid "" -"An invitation from %(user)s to join the room %(room)s has been declined " -"according to your personal settings." -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:185 sat/plugins/plugin_xep_0249.py:192 -#, fuzzy -msgid "MUC invitation" -msgstr "Connexion..." - -#: sat/plugins/plugin_xep_0249.py:188 -#, python-format -msgid "" -"You have been invited by %(user)s to join the room %(room)s. Do you " -"accept?" -msgstr "" - -#: sat/plugins/plugin_xep_0249.py:215 -msgid "You must provide a valid JID to invite, like in '/invite contact@{host}'" -msgstr "" - -#: sat/plugins/plugin_xep_0260.py:51 -#, fuzzy -msgid "Implementation of Jingle SOCKS5 Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: sat/plugins/plugin_xep_0260.py:64 -msgid "plugin Jingle SOCKS5 Bytestreams" -msgstr "" - -#: sat/plugins/plugin_xep_0261.py:47 -#, fuzzy -msgid "Implementation of Jingle In-Band Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: sat/plugins/plugin_xep_0261.py:55 -#, fuzzy -msgid "plugin Jingle In-Band Bytestreams" -msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" - -#: sat/plugins/plugin_xep_0264.py:67 -msgid "Thumbnails handling" -msgstr "" - -#: sat/plugins/plugin_xep_0264.py:79 -#, fuzzy -msgid "Plugin XEP_0264 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0277.py:71 -#, fuzzy -msgid "Implementation of microblogging Protocol" -msgstr "Implémentation du protocole de transports" - -#: sat/plugins/plugin_xep_0277.py:83 -#, fuzzy -msgid "Microblogging plugin initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0277.py:286 -msgid "Content of type XHTML must declare its namespace!" -msgstr "" - -#: sat/plugins/plugin_xep_0277.py:557 -msgid "Can't have xhtml and rich content at the same time" -msgstr "" - -#: sat/plugins/plugin_xep_0277.py:1041 -#, python-format -msgid "Microblog node has now access %s" -msgstr "" - -#: sat/plugins/plugin_xep_0277.py:1045 -msgid "Can't set microblog access" -msgstr "" - -#: sat/plugins/plugin_xep_0280.py:39 -#, fuzzy, python-format -msgid "Message carbons" -msgstr "message reçu de: %s" - -#: sat/plugins/plugin_xep_0280.py:50 -#, fuzzy -msgid "Implementation of Message Carbons" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0280.py:75 -#, fuzzy -msgid "Plugin XEP_0280 initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0280.py:102 -msgid "Not activating message carbons as requested in params" -msgstr "" - -#: sat/plugins/plugin_xep_0280.py:107 -msgid "server doesn't handle message carbons" -msgstr "" - -#: sat/plugins/plugin_xep_0280.py:109 -msgid "message carbons available, enabling it" -msgstr "" - -#: sat/plugins/plugin_xep_0280.py:117 -#, fuzzy -msgid "message carbons activated" -msgstr "" -"Barre de progression désactivée\n" -"--\n" - -#: sat/plugins/plugin_xep_0297.py:44 -#, fuzzy -msgid "Implementation of Stanza Forwarding" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0297.py:52 -#, fuzzy -msgid "Stanza Forwarding plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0300.py:45 -msgid "Management of cryptographic hashes" -msgstr "" - -#: sat/plugins/plugin_xep_0300.py:66 -#, fuzzy -msgid "plugin Hashes initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0313.py:51 -#, fuzzy -msgid "Implementation of Message Archive Management" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0313.py:64 -#, fuzzy -msgid "Message Archive Management plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0313.py:92 -msgid "It seems that we have no MAM history yet" -msgstr "" - -#: sat/plugins/plugin_xep_0313.py:126 -msgid "missing \"to\" attribute in forwarded message" -msgstr "" - -#: sat/plugins/plugin_xep_0313.py:137 -msgid "missing \"from\" attribute in forwarded message" -msgstr "" - -#: sat/plugins/plugin_xep_0313.py:140 -msgid "" -"was expecting a message sent by our jid, but this one if from {from_jid}," -" ignoring\n" -"{xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0313.py:158 -msgid "We have received no message while offline" -msgstr "" - -#: sat/plugins/plugin_xep_0313.py:160 -msgid "We have received {num_mess} message(s) while offline." -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:50 -#, fuzzy -msgid "Implementation of File Information Sharing" -msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: sat/plugins/plugin_xep_0329.py:86 -msgid "path change chars found in name [{name}], hack attempt?" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:107 -msgid "path can only be set on path nodes" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:141 -msgid "a node can't have several parents" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:210 -msgid "" -"parent dir (\"..\") found in path, hack attempt? path is {path} " -"[{profile}]" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:271 -#, fuzzy -msgid "File Information Sharing initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0329.py:394 -msgid "invalid path: {path}" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:428 -msgid "{peer_jid} requested a file (s)he can't access [{profile}]" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:461 -#, fuzzy, python-format -msgid "error while retrieving files: {msg}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat/plugins/plugin_xep_0329.py:513 -msgid "ignoring invalid unicode name ({name}): {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:534 -msgid "unexpected type: {type}" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:573 -#, fuzzy, python-format -msgid "unknown node type: {type}" -msgstr "Type d'action inconnu" - -#: sat/plugins/plugin_xep_0329.py:711 -msgid "unexpected element, ignoring: {elt}" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:1184 -#, fuzzy, python-format -msgid "This path doesn't exist!" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat/plugins/plugin_xep_0329.py:1186 -msgid "A path need to be specified" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:1188 -msgid "access must be a dict" -msgstr "" - -#: sat/plugins/plugin_xep_0329.py:1200 -#, fuzzy -msgid "Can't find a proper name" -msgstr "Impossible de trouver la VCard de %s" - -#: sat/plugins/plugin_xep_0329.py:1211 -msgid "" -"A directory with this name is already shared, renamed to {new_name} " -"[{profile}]" -msgstr "" - -#: sat/plugins/plugin_xep_0334.py:43 -#, fuzzy -msgid "Implementation of Message Processing Hints" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0334.py:45 -msgid "" -" Frontends can use HINT_* constants in mess_data['extra'] in " -"a serialized 'hints' dict.\n" -" Internal plugins can use directly addHint([HINT_* " -"constant]).\n" -" Will set mess_data['extra']['history'] to 'skipped' when no " -"store is requested and message is not saved in history." -msgstr "" - -#: sat/plugins/plugin_xep_0334.py:65 -#, fuzzy -msgid "Message Processing Hints plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0346.py:54 -msgid "Handle Pubsub data schemas" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:60 -#, fuzzy -msgid "PubSub Schema initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0346.py:208 -msgid "unspecified schema, we need to request it" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:212 -msgid "" -"no schema specified, and this node has no schema either, we can't " -"construct the data form" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:233 -msgid "Invalid Schema: {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:246 -msgid "nodeIndentifier needs to be set" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:310 -msgid "empty node is not allowed" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:354 -msgid "default_node must be set if nodeIdentifier is not set" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:457 -#, fuzzy -msgid "field {name} doesn't exist, ignoring it" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat/plugins/plugin_xep_0346.py:551 -msgid "Can't parse date field: {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:652 -msgid "Can't get previous item, update ignored: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:661 -msgid "Can't parse previous item, update ignored: data form not found" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:719 -msgid "default_node must be set if node is not set" -msgstr "" - -#: sat/plugins/plugin_xep_0346.py:728 -msgid "if extra[\"update\"] is set, item_id must be set too" -msgstr "" - -#: sat/plugins/plugin_xep_0352.py:35 -msgid "" -"Notify server when frontend is not actively used, to limit traffic and " -"save bandwidth and battery life" -msgstr "" - -#: sat/plugins/plugin_xep_0352.py:45 -#, fuzzy -msgid "Client State Indication plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0352.py:63 -msgid "Client State Indication is available on this server" -msgstr "" - -#: sat/plugins/plugin_xep_0352.py:67 -msgid "" -"Client State Indication is not available on this server, some bandwidth " -"optimisations can't be used." -msgstr "" - -#: sat/plugins/plugin_xep_0353.py:46 -#, fuzzy -msgid "Implementation of Jingle Message Initiation" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0353.py:53 -#, fuzzy -msgid "plugin {name} initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0353.py:122 -msgid "Message initiation with {peer_jid} timed out" -msgstr "" - -#: sat/plugins/plugin_xep_0353.py:166 -msgid "" -"Somebody not in your contact list ({peer_jid}) wants to do a " -"\"{human_name}\" session with you, this would leak your presence and " -"possibly you IP (internet localisation), do you accept?" -msgstr "" - -#: sat/plugins/plugin_xep_0353.py:171 -#, fuzzy -msgid "Invitation from an unknown contact" -msgstr "Tentative d'assigner un paramètre à un profile inconnu" - -#: sat/plugins/plugin_xep_0353.py:211 -msgid "no pending session found with id {session_id}, did it timed out?" -msgstr "" - -#: sat/plugins/plugin_xep_0359.py:40 -#, fuzzy -msgid "Implementation of Unique and Stable Stanza IDs" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0359.py:49 -#, fuzzy -msgid "Unique and Stable Stanza IDs plugin initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/plugins/plugin_xep_0363.py:51 -#, fuzzy -msgid "Implementation of HTTP File Upload" -msgstr "Implémentation de l'initialisation de flux pour le transfert de fichier " - -#: sat/plugins/plugin_xep_0363.py:83 -#, fuzzy -msgid "plugin HTTP File Upload initialization" -msgstr "Initialisation du plugin XEP_0054" - -#: sat/plugins/plugin_xep_0363.py:200 -msgid "Can't get upload slot: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0363.py:265 -msgid "upload failed: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0363.py:349 -msgid "Invalid header element: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0363.py:355 -msgid "Ignoring unauthorised header \"{name}\": {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0363.py:400 -msgid "no service can handle HTTP Upload request: {elt}" -msgstr "" - -#: sat/plugins/plugin_xep_0380.py:35 -#, fuzzy -msgid "Implementation of Explicit Message Encryption" -msgstr "Implémentation de l'enregistrement en ligne" - -#: sat/plugins/plugin_xep_0380.py:94 -msgid "" -"Message from {sender} is encrypted with {algorithm} and we can't decrypt " -"it." -msgstr "" - -#: sat/plugins/plugin_xep_0380.py:96 -msgid "" -"User {sender} sent you an encrypted message (encrypted with {algorithm})," -" and we can't decrypt it." -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:62 -#, fuzzy -msgid "Implementation of OMEMO" -msgstr "Implementation de vcard-temp" - -#: sat/plugins/plugin_xep_0384.py:440 -msgid "Security" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:442 -msgid "OMEMO default trust policy" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:443 -msgid "Manual trust (more secure)" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:445 -msgid "Blind Trust Before Verification (more user friendly)" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:449 -msgid "OMEMO plugin initialization (omemo module v{version})" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:453 -msgid "" -"Your version of omemo module is too old: {v[0]}.{v[1]}.{v[2]} is minimum " -"required, please update." -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:488 -msgid "You need to have OMEMO encryption activated to reset the session" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:503 -msgid "OMEMO session has been reset" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:551 -msgid "device {device} from {peer_jid} is not an auto-trusted device anymore" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:612 -msgid "Can't find bundle for device {device_id} of user {bare_jid}, ignoring" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:642 -#, fuzzy -msgid "OMEMO trust management" -msgstr "Initialisation du gestionnaire de mémoire" - -#: sat/plugins/plugin_xep_0384.py:645 -msgid "" -"This is OMEMO trusting system. You'll see below the devices of your " -"contacts, and a checkbox to trust them or not. A trusted device can read " -"your messages in plain text, so be sure to only validate devices that you" -" are sure are belonging to your contact. It's better to do this when you " -"are next to your contact and her/his device, so you can check the " -"\"fingerprint\" (the number next to the device) yourself. Do *not* " -"validate a device if the fingerprint is wrong!" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:655 -msgid "This device ID" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:657 -msgid "This device fingerprint" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:669 -msgid "Automatically trust new devices?" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:683 -#, fuzzy -msgid "Contact" -msgstr "&Contacts" - -#: sat/plugins/plugin_xep_0384.py:685 -msgid "Device ID" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:691 -msgid "Trust this device?" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:696 -msgid "(automatically trusted)" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:724 -msgid "We have no identity for this device yet, let's generate one" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:749 -msgid "Saving public bundle for this device ({device_id})" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:770 -msgid "OMEMO devices list is stored in more that one items, this is not expected" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:776 -msgid "no list element found in OMEMO devices list" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:782 -msgid "device element is missing \"id\" attribute: {elt}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:785 -msgid "invalid device id: {device_id}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:804 -msgid "there is no node to handle OMEMO devices" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:826 -msgid "Can't set devices: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:850 -msgid "Bundle missing for device {device_id}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:855 -msgid "Can't get bundle for device {device_id}: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:859 -msgid "" -"no item found in node {node}, can't get public bundle for device " -"{device_id}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:864 -msgid "more than one item found in {node}, this is not expected" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:878 -msgid "invalid bundle for device {device_id}, ignoring" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:902 -msgid "error while decoding key for device {device_id}: {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:916 -msgid "updating bundle for {device_id}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:949 -msgid "Can't set bundle: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:969 -msgid "Our own device is missing from devices list, fixing it" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:988 -msgid "" -"Not all destination devices are trusted, unknown devices will be blind " -"trusted due to the OMEMO Blind Trust Before Verification policy. If you " -"want a more secure workflow, please activate \"manual\" OMEMO policy in " -"settings' \"Security\" tab.\n" -"Following fingerprint have been automatically trusted:\n" -"{devices}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1010 -msgid "" -"Not all destination devices are trusted, we can't encrypt message in such" -" a situation. Please indicate if you trust those devices or not in the " -"trust manager before we can send this message" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1053 -msgid "discarding untrusted device {device_id} with key {device_key} for {entity}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1095 -msgid "" -"Can't retrieve bundle for device(s) {devices} of entity {peer}, the " -"message will not be readable on this/those device(s)" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1100 -msgid "" -"You're destinee {peer} has missing encryption data on some of his/her " -"device(s) (bundle on device {devices}), the message won't be readable on" -" this/those device." -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1151 -msgid "Too many iterations in encryption loop" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1180 -msgid "Can't encrypt message for {entities}: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1270 -msgid "Invalid OMEMO encrypted stanza, ignoring: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1276 -msgid "Invalid OMEMO encrypted stanza, missing sender device ID, ignoring: {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1284 -msgid "" -"This OMEMO encrypted stanza has not been encrypted for our device " -"(device_id: {device_id}, fingerprint: {fingerprint}): {xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1290 -msgid "" -"An OMEMO message from {sender} has not been encrypted for our device, we " -"can't decrypt it" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1297 -msgid "Invalid recipient ID: {msg}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1330 -msgid "" -"Can't decrypt message: {reason}\n" -"{xml}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1332 -msgid "An OMEMO message from {sender} can't be decrypted: {reason}" -msgstr "" - -#: sat/plugins/plugin_xep_0384.py:1364 -msgid "" -"Our message with UID {uid} has not been received in time, it has probably" -" been lost. The message was: {msg!r}" -msgstr "" - -#: sat/plugins/plugin_app_manager_docker/__init__.py:38 -msgid "Applications Manager for Docker" -msgstr "" - -#: sat/plugins/plugin_app_manager_docker/__init__.py:48 -#, fuzzy -msgid "Docker App Manager initialization" -msgstr "Initialisation de l'extension pour les transports" - -#: sat/stdui/ui_contact_list.py:39 sat/stdui/ui_contact_list.py:42 -#: sat/stdui/ui_contact_list.py:190 sat/stdui/ui_contact_list.py:276 -#, fuzzy -msgid "Add contact" -msgstr "&Ajouter un contact" - -#: sat/stdui/ui_contact_list.py:45 sat/stdui/ui_contact_list.py:48 -#: sat/stdui/ui_contact_list.py:209 -#, fuzzy -msgid "Update contact" -msgstr "&Ajouter un contact" - -#: sat/stdui/ui_contact_list.py:51 sat/stdui/ui_contact_list.py:54 -#, fuzzy -msgid "Remove contact" -msgstr "Supp&rimer un contact" - -#: sat/stdui/ui_contact_list.py:157 -msgid "Select in which groups your contact is:" -msgstr "" - -#: sat/stdui/ui_contact_list.py:172 -msgid "Add group" -msgstr "" - -#: sat/stdui/ui_contact_list.py:174 -msgid "Add" -msgstr "" - -#: sat/stdui/ui_contact_list.py:191 -#, fuzzy, python-format -msgid "New contact identifier (JID):" -msgstr "nouveau contact: %s" - -#: sat/stdui/ui_contact_list.py:203 -msgid "Nothing to update" -msgstr "" - -#: sat/stdui/ui_contact_list.py:204 sat/stdui/ui_contact_list.py:223 -msgid "Your contact list is empty." -msgstr "" - -#: sat/stdui/ui_contact_list.py:210 -msgid "Which contact do you want to update?" -msgstr "" - -#: sat/stdui/ui_contact_list.py:222 -msgid "Nothing to delete" -msgstr "" - -#: sat/stdui/ui_contact_list.py:228 -#, fuzzy, python-format -msgid "Who do you want to remove from your contacts?" -msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" - -#: sat/stdui/ui_contact_list.py:251 -#, fuzzy -msgid "Delete contact" -msgstr "&Ajouter un contact" - -#: sat/stdui/ui_contact_list.py:253 -#, fuzzy, python-format -msgid "Are you sure you want to remove %s from your contact list?" -msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" - -#: sat/stdui/ui_contact_list.py:277 -#, python-format -msgid "Please enter a valid JID (like \"contact@%s\"):" -msgstr "" - -#: sat/stdui/ui_profile_manager.py:62 -msgid "Profile password for {}" -msgstr "" - -#: sat/stdui/ui_profile_manager.py:72 sat/stdui/ui_profile_manager.py:119 -#, fuzzy -msgid "Connection error" -msgstr "Connexion..." - -#: sat/stdui/ui_profile_manager.py:76 -#: sat_frontends/quick_frontend/quick_profile_manager.py:171 -#, fuzzy -msgid "Internal error" -msgstr "Transfert de fichier" - -#: sat/stdui/ui_profile_manager.py:77 -msgid "Internal error: {}" -msgstr "" - -#: sat/stdui/ui_profile_manager.py:121 -#, python-format -msgid "Can't connect to %s. Please check your connection details." -msgstr "" - -#: sat/stdui/ui_profile_manager.py:127 -#, python-format -msgid "XMPP password for %(profile)s%(counter)s" -msgstr "" - -#: sat/stdui/ui_profile_manager.py:135 -#, python-format -msgid "" -"Can't connect to %s. Please check your connection details or try with " -"another password." -msgstr "" - -#: sat/test/constants.py:57 -msgid "Enable unibox" -msgstr "" - -#: sat/test/constants.py:58 -msgid "'Wysiwyg' edition" -msgstr "" - -#: sat/test/test_plugin_misc_room_game.py:43 -msgid "Dummy plugin to test room game" -msgstr "" - -#: sat/tools/config.py:53 -#, fuzzy, python-format -msgid "Testing file %s" -msgstr "Échec de l'inscription: %s" - -#: sat/tools/config.py:72 -msgid "Config auto-update: {option} set to {value} in the file {config_file}." -msgstr "" - -#: sat/tools/config.py:86 -msgid "Can't read main config: {msg}" -msgstr "" - -#: sat/tools/config.py:91 -msgid "Configuration was read from: {filenames}" -msgstr "" - -#: sat/tools/config.py:95 -#, fuzzy, python-format -msgid "No configuration file found, using default settings" -msgstr "Disposition inconnue, utilisation de celle par defaut" - -#: sat/tools/image.py:35 -msgid "SVG support not available, please install cairosvg: {e}" -msgstr "" - -#: sat/tools/trigger.py:66 -#, python-format -msgid "There is already a bound priority [%s]" -msgstr "" - -#: sat/tools/trigger.py:69 -#, python-format -msgid "There is already a trigger with the same priority [%s]" -msgstr "" - -#: sat/tools/video.py:38 -msgid "ffmpeg executable not found, video thumbnails won't be available" -msgstr "" - -#: sat/tools/video.py:56 -msgid "ffmpeg executable is not available, can't generate video thumbnail" -msgstr "" - -#: sat/tools/xml_tools.py:86 -msgid "Fixed field has neither value nor label, ignoring it" -msgstr "" - -#: sat/tools/xml_tools.py:485 -#, fuzzy -msgid "INTERNAL ERROR: parameters xml not valid" -msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" - -#: sat/tools/xml_tools.py:495 -msgid "INTERNAL ERROR: params categories must have a name" -msgstr "ERREUR INTERNE: les catégories des paramètres doivent avoir un nom" - -#: sat/tools/xml_tools.py:505 -msgid "INTERNAL ERROR: params must have a name" -msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" - -#: sat/tools/xml_tools.py:557 -msgid "The 'options' tag is not allowed in parameter of type 'list'!" -msgstr "" - -#: sat/tools/xml_tools.py:655 -msgid "TabElement must be a child of TabsContainer" -msgstr "" - -#: sat/tools/xml_tools.py:760 -msgid "Can't set row index if auto_index is True" -msgstr "" - -#: sat/tools/xml_tools.py:893 -msgid "either items or columns need do be filled" -msgstr "" - -#: sat/tools/xml_tools.py:907 -msgid "Headers lenght doesn't correspond to columns" -msgstr "" - -#: sat/tools/xml_tools.py:954 -msgid "Incorrect number of items in list" -msgstr "" - -#: sat/tools/xml_tools.py:978 -#, fuzzy -msgid "A widget with the name \"{name}\" already exists." -msgstr "Ce nom de profile existe déjà" - -#: sat/tools/xml_tools.py:1171 -msgid "Value must be an integer" -msgstr "" - -#: sat/tools/xml_tools.py:1186 -msgid "Value must be 0, 1, false or true" -msgstr "" - -#: sat/tools/xml_tools.py:1249 -msgid "" -"\"multi\" flag and \"selected\" option are not compatible with " -"\"noselect\" flag" -msgstr "" - -#: sat/tools/xml_tools.py:1258 -msgid "empty \"options\" list" -msgstr "" - -#: sat/tools/xml_tools.py:1277 sat/tools/xml_tools.py:1311 -msgid "invalid styles" -msgstr "" - -#: sat/tools/xml_tools.py:1335 -msgid "DialogElement must be a direct child of TopElement" -msgstr "" - -#: sat/tools/xml_tools.py:1350 -msgid "MessageElement must be a direct child of DialogElement" -msgstr "" - -#: sat/tools/xml_tools.py:1365 -msgid "ButtonsElement must be a direct child of DialogElement" -msgstr "" - -#: sat/tools/xml_tools.py:1379 -msgid "FileElement must be a direct child of DialogElement" -msgstr "" - -#: sat/tools/xml_tools.py:1458 -#, fuzzy, python-format -msgid "Unknown panel type [%s]" -msgstr "Type d'action inconnu" - -#: sat/tools/xml_tools.py:1460 -msgid "form XMLUI need a submit_id" -msgstr "" - -#: sat/tools/xml_tools.py:1462 -msgid "container argument must be a string" -msgstr "" - -#: sat/tools/xml_tools.py:1465 -msgid "dialog_opt can only be used with dialog panels" -msgstr "" - -#: sat/tools/xml_tools.py:1492 -msgid "createWidget can't be used with dialogs" -msgstr "" - -#: sat/tools/xml_tools.py:1590 -msgid "Submit ID must be filled for this kind of dialog" -msgstr "" - -#: sat/tools/xml_tools.py:1618 -#, fuzzy, python-format -msgid "Unknown container type [%s]" -msgstr "Type d'action inconnu" - -#: sat/tools/xml_tools.py:1648 -#, fuzzy, python-format -msgid "Invalid type [{type_}]" -msgstr "Type d'action inconnu" - -#: sat/tools/common/async_process.py:86 -msgid "" -"Can't complete {name} command (error code: {code}):\n" -"stderr:\n" -"{stderr}\n" -"{stdout}\n" -msgstr "" - -#: sat/tools/common/date_utils.py:76 -msgid "You can't use a direction (+ or -) and \"ago\" at the same time" -msgstr "" - -#: sat/tools/common/template.py:149 -msgid "{site} can't be used as site name, it's reserved." -msgstr "" - -#: sat/tools/common/template.py:157 -msgid "{theme} contain forbidden char. Following chars are forbidden: {reserved}" -msgstr "" - -#: sat/tools/common/template.py:212 -msgid "Unregistered site requested: {site_to_check}" -msgstr "" - -#: sat/tools/common/template.py:241 -msgid "" -"Absolute template used while unsecure is disabled, hack attempt? " -"Template: {template}" -msgstr "" - -#: sat/tools/common/template.py:314 -msgid "Invalid attribute, please use one of \"defer\", \"async\" or \"\"" -msgstr "" - -#: sat/tools/common/template.py:332 -msgid "Can't find {libary} javascript library" -msgstr "" - -#: sat/tools/common/template.py:389 -msgid "" -"Can't add \"{name}\" site, it contains forbidden characters. Forbidden " -"characters are {forbidden}." -msgstr "" - -#: sat/tools/common/template.py:395 -msgid "Can't add \"{name}\" site, it should map to an absolute path" -msgstr "" - -#: sat/tools/common/template.py:416 -msgid "Can't load theme settings at {path}: {e}" -msgstr "" - -#: sat/tools/common/template.py:523 -msgid "Can't find template translation at {path}" -msgstr "" - -#: sat/tools/common/template.py:526 -msgid "{site}Invalid locale name: {msg}" -msgstr "" - -#: sat/tools/common/template.py:529 -msgid "{site}loaded {lang} templates translations" -msgstr "" - -#: sat/tools/common/template.py:560 -msgid "invalid locale value: {msg}" -msgstr "" - -#: sat/tools/common/template.py:569 -msgid "Can't find locale {locale}" -msgstr "" - -#: sat/tools/common/template.py:574 -msgid "Switched to {lang}" -msgstr "" - -#: sat/tools/common/template.py:774 sat_frontends/jp/cmd_event.py:134 -msgid "Can't parse date: {msg}" -msgstr "" - -#: sat/tools/common/template.py:801 -#, fuzzy -msgid "ignoring field \"{name}\": it doesn't exists" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/arg_tools.py:88 -msgid "ignoring {name}={value}, not corresponding to any argument (in USE)" -msgstr "" - -#: sat_frontends/jp/arg_tools.py:95 -msgid "arg {name}={value} (in USE)" -msgstr "" - -#: sat_frontends/jp/base.py:64 -#, fuzzy -msgid "" -"ProgressBar not available, please download it at " -"http://pypi.python.org/pypi/progressbar\n" -"Progress bar deactivated\n" -"--\n" -msgstr "" -"ProgressBar n'est pas disponible, veuillez le télécharger à " -"http://pypi.python.org/pypi/progressbar" - -#: sat_frontends/jp/base.py:155 -msgid "" -"Invalid value set for \"background\" ({background}), please check your " -"settings in libervia.conf" -msgstr "" - -#: sat_frontends/jp/base.py:178 -msgid "Available commands" -msgstr "" - -#: sat_frontends/jp/base.py:287 -#, python-format -msgid "Use PROFILE profile key (default: %(default)s)" -msgstr "" - -#: sat_frontends/jp/base.py:290 -msgid "Password used to connect profile, if necessary" -msgstr "" - -#: sat_frontends/jp/base.py:297 -msgid "Connect the profile before doing anything else" -msgstr "" - -#: sat_frontends/jp/base.py:307 -msgid "Start a profile session without connecting" -msgstr "" - -#: sat_frontends/jp/base.py:313 -msgid "Show progress bar" -msgstr "Affiche la barre de progression" - -#: sat_frontends/jp/base.py:318 -msgid "Add a verbosity level (can be used multiple times)" -msgstr "" - -#: sat_frontends/jp/base.py:323 -msgid "be quiet (only output machine readable data)" -msgstr "" - -#: sat_frontends/jp/base.py:326 -msgid "draft handling" -msgstr "" - -#: sat_frontends/jp/base.py:328 -msgid "load current draft" -msgstr "" - -#: sat_frontends/jp/base.py:330 -msgid "path to a draft file to retrieve" -msgstr "" - -#: sat_frontends/jp/base.py:346 -msgid "Pubsub URL (xmpp or http)" -msgstr "" - -#: sat_frontends/jp/base.py:348 -#, fuzzy -msgid "JID of the PubSub service" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/base.py:350 -msgid "PEP service" -msgstr "" - -#: sat_frontends/jp/base.py:352 sat_frontends/jp/base.py:360 -#: sat_frontends/jp/base.py:368 -msgid " (DEFAULT: {default})" -msgstr "" - -#: sat_frontends/jp/base.py:356 -#, fuzzy -msgid "node to request" -msgstr "Demande de suppression de contact" - -#: sat_frontends/jp/base.py:358 -msgid "standard node" -msgstr "" - -#: sat_frontends/jp/base.py:366 -msgid "last item" -msgstr "" - -#: sat_frontends/jp/base.py:372 -msgid "retrieve last item" -msgstr "" - -#: sat_frontends/jp/base.py:378 -msgid "items to retrieve (DEFAULT: all)" -msgstr "" - -#: sat_frontends/jp/base.py:385 -msgid "maximum number of items to get ({no_limit} to get all items)" -msgstr "" - -#: sat_frontends/jp/base.py:391 -msgid "maximum number of items to get per page (DEFAULT: 10)" -msgstr "" - -#: sat_frontends/jp/base.py:398 sat_frontends/jp/cmd_message.py:217 -msgid "find page after this item" -msgstr "" - -#: sat_frontends/jp/base.py:401 sat_frontends/jp/cmd_message.py:220 -msgid "find page before this item" -msgstr "" - -#: sat_frontends/jp/base.py:404 sat_frontends/jp/cmd_message.py:223 -msgid "index of the page to retrieve" -msgstr "" - -#: sat_frontends/jp/base.py:411 -#, fuzzy -msgid "MAM filters to use" -msgstr "Veuillez choisir le fichier à envoyer" - -#: sat_frontends/jp/base.py:424 -msgid "how items should be ordered" -msgstr "" - -#: sat_frontends/jp/base.py:454 -msgid "there is already a default output for {type}, ignoring new one" -msgstr "" - -#: sat_frontends/jp/base.py:475 -msgid "The following output options are invalid: {invalid_options}" -msgstr "" - -#: sat_frontends/jp/base.py:499 -msgid "Can't import {module_path} plugin, ignoring it: {e}" -msgstr "" - -#: sat_frontends/jp/base.py:505 -msgid "Missing module for plugin {name}: {missing}" -msgstr "" - -#: sat_frontends/jp/base.py:520 -msgid "Invalid plugin module [{type}] {module}" -msgstr "" - -#: sat_frontends/jp/base.py:552 -msgid "Can't parse HTML page : {msg}" -msgstr "" - -#: sat_frontends/jp/base.py:558 -msgid "" -"Could not find alternate \"xmpp:\" URI, can't find associated XMPP PubSub" -" node/item" -msgstr "" - -#: sat_frontends/jp/base.py:576 -msgid "invalid XMPP URL: {url}" -msgstr "" - -#: sat_frontends/jp/base.py:596 -msgid "item specified in URL but not needed in command, ignoring it" -msgstr "" - -#: sat_frontends/jp/base.py:612 -msgid "XMPP URL is not a pubsub one: {url}" -msgstr "" - -#: sat_frontends/jp/base.py:618 -msgid "argument -s/--service is required" -msgstr "" - -#: sat_frontends/jp/base.py:620 -msgid "argument -n/--node is required" -msgstr "" - -#: sat_frontends/jp/base.py:622 -msgid "argument -i/--item is required" -msgstr "" - -#: sat_frontends/jp/base.py:629 -msgid "--item and --item-last can't be used at the same time" -msgstr "" - -#: sat_frontends/jp/base.py:659 sat_frontends/quick_frontend/quick_app.py:370 -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é ?" - -#: sat_frontends/jp/base.py:662 sat_frontends/quick_frontend/quick_app.py:373 -#, fuzzy -msgid "Can't init bridge" -msgstr "Construction du jeu de Tarot" - -#: sat_frontends/jp/base.py:666 -msgid "Error while initialising bridge: {e}" -msgstr "" - -#: sat_frontends/jp/base.py:714 -msgid "action cancelled by user" -msgstr "" - -#: sat_frontends/jp/base.py:785 -#, python-format -msgid "%s is not a valid JID !" -msgstr "%s n'est pas un JID valide !" - -#: sat_frontends/jp/base.py:837 -#, fuzzy -msgid "invalid password" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat_frontends/jp/base.py:839 -#, fuzzy -msgid "please enter profile password:" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/jp/base.py:859 -#, fuzzy, python-format -msgid "The profile [{profile}] doesn't exist" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/base.py:881 -#, fuzzy, python-format -msgid "" -"Session for [{profile}] is not started, please start it before using jp, " -"or use either --start-session or --connect option" -msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" - -#: sat_frontends/jp/base.py:901 -#, fuzzy, python-format -msgid "" -"Profile [{profile}] is not connected, please connect it before using jp, " -"or use --connect option" -msgstr "SAT n'est pas connecté, veuillez le connecter avant d'utiliser jp" - -#: sat_frontends/jp/base.py:1002 -msgid "select output format (default: {})" -msgstr "" - -#: sat_frontends/jp/base.py:1005 -msgid "output specific option" -msgstr "" - -#: sat_frontends/jp/base.py:1111 -msgid "file size is not known, we can't show a progress bar" -msgstr "" - -#: sat_frontends/jp/base.py:1126 sat_frontends/jp/cmd_list.py:304 -msgid "Progress: " -msgstr "Progression: " - -#: sat_frontends/jp/base.py:1156 -#, fuzzy -msgid "Operation started" -msgstr "inscription demandée pour" - -#: sat_frontends/jp/base.py:1172 -#, fuzzy, python-format -msgid "Operation successfully finished" -msgstr "Transfert [%s] refusé" - -#: sat_frontends/jp/base.py:1179 -msgid "Error while doing operation: {e}" -msgstr "" - -#: sat_frontends/jp/base.py:1189 -msgid "trying to use output when use_output has not been set" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:42 -msgid "create a XMPP account" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:47 -msgid "jid to create" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:50 -msgid "password of the account" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:55 -msgid "create a profile to use this account (default: don't create profile)" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:63 -msgid "email (usage depends of XMPP server)" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:69 -msgid "server host (IP address or domain, default: use localhost)" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:76 -msgid "server port (default: {port})" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:107 -msgid "XMPP account created" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:113 -#, fuzzy -msgid "creating profile" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/jp/cmd_account.py:129 -msgid "Can't create profile {profile} to associate with jid {jid}: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:142 -#, fuzzy -msgid "profile created" -msgstr "Aucun profile sélectionné" - -#: sat_frontends/jp/cmd_account.py:183 -msgid "change password for XMPP account" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:188 -#, fuzzy -msgid "new XMPP password" -msgstr "Sauvegarde du nouveau mot de passe" - -#: sat_frontends/jp/cmd_account.py:207 -msgid "delete a XMPP account" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:215 -msgid "delete account without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_account.py:236 -msgid "Account deletion cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:34 -#, fuzzy -msgid "remote control a software" -msgstr "Supp&rimer un contact" - -#: sat_frontends/jp/cmd_adhoc.py:38 -msgid "software name" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:44 -msgid "jids allowed to use the command" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:51 -msgid "groups allowed to use the command" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:57 -msgid "groups that are *NOT* allowed to use the command" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:63 -msgid "jids that are *NOT* allowed to use the command" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:66 -#, fuzzy -msgid "loop on the commands" -msgstr "Mauvais nom de profile" - -#: sat_frontends/jp/cmd_adhoc.py:93 -#, fuzzy, python-format -msgid "No bus name found" -msgstr "Fonctionnalité trouvée: %s" - -#: sat_frontends/jp/cmd_adhoc.py:96 -#, fuzzy, python-format -msgid "Bus name found: [%s]" -msgstr "Fonctionnalité trouvée: %s" - -#: sat_frontends/jp/cmd_adhoc.py:100 -msgid "Command found: (path:{path}, iface: {iface}) [{command}]" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:112 -msgid "run an Ad-Hoc command" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:120 sat_frontends/jp/cmd_message.py:200 -msgid "jid of the service (default: profile's server" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:128 -msgid "submit form/page" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:137 -msgid "field value" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:143 -msgid "node of the command (default: list commands)" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:171 -msgid "list Ad-Hoc commands of a service" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:179 -msgid "jid of the service (default: profile's server)" -msgstr "" - -#: sat_frontends/jp/cmd_adhoc.py:202 -msgid "Ad-hoc commands" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:33 -msgid "list available applications" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:45 -msgid "show applications with this status" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:72 -#, fuzzy -msgid "start an application" -msgstr "Sélection du contrat" - -#: sat_frontends/jp/cmd_application.py:78 -msgid "name of the application to start" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:98 -msgid "stop a running application" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:106 -msgid "name of the application to stop" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:111 -msgid "identifier of the instance to stop" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:142 -msgid "show data exposed by a running application" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:150 -msgid "name of the application to check" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:155 -msgid "identifier of the instance to check" -msgstr "" - -#: sat_frontends/jp/cmd_application.py:189 -#, fuzzy -msgid "manage applications" -msgstr "Tab inconnu" - -#: sat_frontends/jp/cmd_avatar.py:38 -msgid "retrieve avatar of an entity" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:43 sat_frontends/jp/cmd_identity.py:42 -msgid "do no use cached values" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:46 -msgid "show avatar" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:48 sat_frontends/jp/cmd_info.py:111 -#, fuzzy -msgid "entity" -msgstr "Petite" - -#: sat_frontends/jp/cmd_avatar.py:87 -#, fuzzy -msgid "No avatar found." -msgstr "Aucune donnée trouvée" - -#: sat_frontends/jp/cmd_avatar.py:103 -msgid "set avatar of the profile or an entity" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:108 -msgid "entity whose avatar must be changed" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:110 -msgid "path to the image to upload" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:116 -#, fuzzy, python-format -msgid "file {path} doesn't exist!" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/cmd_avatar.py:125 -msgid "avatar has been set" -msgstr "" - -#: sat_frontends/jp/cmd_avatar.py:134 -msgid "avatar uploading/retrieving" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:129 -msgid "unknown syntax requested ({syntax})" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:138 -#, fuzzy -msgid "title of the item" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_blog.py:143 -msgid "tag (category) of your item" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:148 -msgid "language of the item (ISO 639 code)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:158 -msgid "" -"enable comments (default: comments not enabled except if they already " -"exist)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:168 -msgid "disable comments (will remove comments node if it exist)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:174 -msgid "syntax to use (default: get profile's default syntax)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:211 -msgid "publish a new blog item or update an existing one" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:257 -msgid "get blog item(s)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:267 -msgid "microblog data key(s) to display (default: depend of verbosity)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:462 -msgid "edit an existing or new blog post" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:473 -msgid "launch a blog preview in parallel" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:478 -msgid "add \"publish: False\" to metadata" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:628 -msgid "You need lxml to edit pretty XHTML" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:662 -msgid "rename an blog item" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:666 sat_frontends/jp/cmd_pubsub.py:996 -msgid "new item id to use" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:690 -msgid "preview a blog content" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:700 -msgid "use inotify to handle preview" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:706 -#, fuzzy -msgid "path to the content file" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_blog.py:810 -#, fuzzy, python-format -msgid "File \"{file}\" doesn't exist!" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/cmd_blog.py:898 -msgid "import an external blog" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:905 sat_frontends/jp/cmd_list.py:207 -msgid "importer name, nothing to display importers list" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:907 -msgid "original blog host" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:911 -msgid "do *NOT* upload images (default: do upload images)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:915 -msgid "do not upload images from this host (default: upload all images)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:920 -msgid "ignore invalide TLS certificate for uploads" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:929 sat_frontends/jp/cmd_list.py:216 -msgid "importer specific options (see importer description)" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:934 sat_frontends/jp/cmd_list.py:250 -msgid "" -"importer data location (see importer description), nothing to show " -"importer description" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:941 -msgid "Blog upload started" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:944 -msgid "Blog uploaded successfully" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:965 -msgid "" -"\n" -"To redirect old URLs to new ones, put the following lines in your " -"sat.conf file, in [libervia] section:\n" -"\n" -"{conf}" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:973 -#, fuzzy, python-format -msgid "Error while uploading blog: {error_msg}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_blog.py:982 sat_frontends/jp/cmd_list.py:274 -msgid "{name} argument can't be used without location argument" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:1037 -msgid "Error while trying to import a blog: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_blog.py:1050 -msgid "blog/microblog management" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:40 -#, python-format -msgid "storage location (default: %(default)s)" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:48 -#, python-format -msgid "bookmarks type (default: %(default)s)" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:54 -msgid "list bookmarks" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:94 -msgid "remove a bookmark" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:99 sat_frontends/jp/cmd_bookmarks.py:131 -msgid "jid (for muc bookmark) or url of to remove" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:105 -msgid "delete bookmark without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:110 -#, fuzzy, python-format -msgid "Are you sure to delete this bookmark?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/jp/cmd_bookmarks.py:117 -msgid "can't delete bookmark: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:120 -msgid "bookmark deleted" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:133 -msgid "bookmark name" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:134 -msgid "MUC specific options" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:135 -#, fuzzy -msgid "nickname" -msgstr "Surnon" - -#: sat_frontends/jp/cmd_bookmarks.py:140 -msgid "join room on profile connection" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:145 -msgid "You can't use --autojoin or --nick with --type url" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:165 -msgid "bookmark successfully added" -msgstr "" - -#: sat_frontends/jp/cmd_bookmarks.py:174 -msgid "manage bookmarks" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:49 -msgid "call a bridge method" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:54 -msgid "name of the method to execute" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:56 -msgid "argument of the method" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:79 -msgid "Error while executing {method}: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:94 -msgid "send a fake signal from backend" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:99 -msgid "name of the signal to send" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:100 -msgid "argument of the signal" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:112 -#, fuzzy, python-format -msgid "Can't send fake signal: {e}" -msgstr "message reçu de: %s" - -#: sat_frontends/jp/cmd_debug.py:123 -msgid "bridge s(t)imulation" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:135 -msgid "monitor XML stream" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:144 -msgid "stream direction filter" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:195 -msgid "print colours used with your background" -msgstr "" - -#: sat_frontends/jp/cmd_debug.py:226 -msgid "debugging tools" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:38 -msgid "show available encryption algorithms" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:45 -msgid "No encryption plugin registered!" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:47 -msgid "Following encryption algorithms are available: {algos}" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:68 -msgid "get encryption session data" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:73 -msgid "jid of the entity to check" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:99 -msgid "start encrypted session with an entity" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:105 sat_frontends/jp/cmd_message.py:77 -msgid "don't replace encryption algorithm if an other one is already used" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:108 -msgid "algorithm name (DEFAULT: choose automatically)" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:111 -msgid "algorithm namespace (DEFAULT: choose automatically)" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:114 -#: sat_frontends/jp/cmd_encryption.py:153 -#: sat_frontends/jp/cmd_encryption.py:178 -msgid "jid of the entity to stop encrypted session with" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:148 -msgid "stop encrypted session with an entity" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:173 -msgid "get UI to manage trust" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:182 -msgid "algorithm name (DEFAULT: current algorithm)" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:185 -msgid "algorithm namespace (DEFAULT: current algorithm)" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:220 -msgid "trust manangement" -msgstr "" - -#: sat_frontends/jp/cmd_encryption.py:230 -msgid "encryption sessions handling" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:47 -msgid "get list of registered events" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:78 -msgid "get event data" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:108 -#, fuzzy -msgid "ID of the PubSub Item" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_event.py:110 -msgid "date of the event" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:118 sat_frontends/jp/cmd_event.py:257 -#: sat_frontends/jp/cmd_pubsub.py:129 -#, fuzzy -msgid "configuration field to set" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_event.py:150 -msgid "create or replace event" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:170 -msgid "Event created successfuly on node {node}" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:181 -msgid "modify an existing event" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:214 sat_frontends/jp/cmd_event.py:288 -msgid "get event attendance" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:219 -#, fuzzy -msgid "bare jid of the invitee" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_event.py:246 -msgid "set event attendance" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:296 -msgid "show missing people (invited but no R.S.V.P. so far)" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:302 -msgid "don't show people which gave R.S.V.P." -msgstr "" - -#: sat_frontends/jp/cmd_event.py:371 -msgid "Attendees: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:374 -msgid " (" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:376 -msgid "yes: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:379 -msgid ", maybe: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:383 -msgid "no: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:390 -msgid "confirmed guests: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:395 -msgid "unconfirmed guests: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:401 -msgid "total: " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:408 -msgid "missing people (no reply): " -msgstr "" - -#: sat_frontends/jp/cmd_event.py:416 -msgid "you need to use --missing if you use --no-rsvp" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:489 -msgid "invite someone to the event through email" -msgstr "" - -#: sat_frontends/jp/cmd_event.py:568 -#, fuzzy -msgid "manage invities" -msgstr "Initialisation du gestionnaire de mémoire" - -#: sat_frontends/jp/cmd_event.py:577 -msgid "event management" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:50 -#, fuzzy -msgid "send a file to a contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat_frontends/jp/cmd_file.py:55 -#, fuzzy -msgid "a list of file" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_file.py:57 sat_frontends/jp/cmd_file.py:191 -#: sat_frontends/jp/cmd_message.py:82 sat_frontends/jp/cmd_pipe.py:42 -msgid "the destination jid" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:59 -#, fuzzy -msgid "make a bzip2 tarball" -msgstr "Fait un fichier compressé bzip2" - -#: sat_frontends/jp/cmd_file.py:79 sat_frontends/jp/cmd_file.py:236 -#: sat_frontends/jp/cmd_file.py:330 -msgid "File copy started" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:82 -#, fuzzy -msgid "File sent successfully" -msgstr "Inscription réussie" - -#: sat_frontends/jp/cmd_file.py:86 -#, fuzzy -msgid "The file has been refused by your contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat_frontends/jp/cmd_file.py:88 -#, fuzzy, python-format -msgid "Error while sending file: {}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_file.py:97 -msgid "File request sent to {jid}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:102 -#, fuzzy -msgid "Can't send file to {jid}" -msgstr "Impossible de trouver la VCard de %s" - -#: sat_frontends/jp/cmd_file.py:109 -#, fuzzy, python-format -msgid "file {file_} doesn't exist!" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/cmd_file.py:114 -#, fuzzy, python-format -msgid "{file_} 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." - -#: sat_frontends/jp/cmd_file.py:129 -#, fuzzy -msgid "bz2 is an experimental option, use with caution" -msgstr "" -"bz2 est une option expérimentale à un stade de développement peu avancé, " -"utilisez-là avec prudence" - -#: sat_frontends/jp/cmd_file.py:131 -msgid "Starting compression, please wait..." -msgstr "Lancement de la compression, veuillez patienter..." - -#: sat_frontends/jp/cmd_file.py:138 -#, fuzzy, python-format -msgid "Adding {}" -msgstr "Ajout de %s" - -#: sat_frontends/jp/cmd_file.py:141 -#, fuzzy -msgid "Done !" -msgstr "N° de Tél:" - -#: sat_frontends/jp/cmd_file.py:183 -#, fuzzy -msgid "request a file from a contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat_frontends/jp/cmd_file.py:195 -msgid "" -"destination path where the file will be saved (default: " -"[current_dir]/[name|hash])" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:204 -#, fuzzy -msgid "name of the file" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_file.py:210 -#, fuzzy -msgid "hash of the file" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_file.py:216 -msgid "hash algorithm use for --hash (default: sha-256)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:232 sat_frontends/jp/cmd_file.py:476 -msgid "overwrite existing file without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:239 sat_frontends/jp/cmd_file.py:333 -#, fuzzy, python-format -msgid "File received successfully" -msgstr "tarot: chien reçu" - -#: sat_frontends/jp/cmd_file.py:243 -msgid "The file request has been refused" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:245 -#, fuzzy, python-format -msgid "Error while requesting file: {}" -msgstr "Échec de la désinscription: %s" - -#: sat_frontends/jp/cmd_file.py:249 -msgid "at least one of --name or --hash must be provided" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:258 sat_frontends/jp/cmd_file.py:510 -msgid "File {path} already exists! Do you want to overwrite?" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:261 -msgid "file request cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:280 -#, fuzzy, python-format -msgid "can't request file: {e}" -msgstr "Échec de la désinscription: %s" - -#: sat_frontends/jp/cmd_file.py:293 -#, fuzzy -msgid "wait for a file to be sent by a contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat_frontends/jp/cmd_file.py:306 -msgid "jids accepted (accept everything if none is specified)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:312 -#, fuzzy -msgid "accept multiple files (you'll have to stop manually)" -msgstr "Accepte plusieurs fichiers (vous devrez arrêter le programme à la main)" - -#: sat_frontends/jp/cmd_file.py:318 -#, fuzzy -msgid "force overwritting of existing files (/!\\ name is choosed by sender)" -msgstr "Force le remplacement des fichiers existants" - -#: sat_frontends/jp/cmd_file.py:326 -msgid "destination path (default: working directory)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:337 -msgid "hash checked: {metadata['hash_algo']}:{metadata['hash']}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:340 -msgid "hash is checked but hash value is missing" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:342 -msgid "hash can't be verified" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:345 -#, fuzzy -msgid "Error while receiving file: {e}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_file.py:354 sat_frontends/jp/cmd_pipe.py:111 -msgid "Action has no XMLUI" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:359 sat_frontends/jp/cmd_pipe.py:115 -msgid "Invalid XMLUI received" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:369 sat_frontends/jp/cmd_pipe.py:126 -msgid "Ignoring action without from_jid data" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:374 sat_frontends/jp/cmd_file.py:395 -msgid "ignoring action without progress id" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:379 -msgid "File refused because overwrite is needed" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:397 -msgid "Overwriting needed" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:401 -#, fuzzy -msgid "Overwrite accepted" -msgstr "accepté" - -#: sat_frontends/jp/cmd_file.py:403 -msgid "Refused to overwrite" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:417 -msgid "invalid \"from_jid\" value received, ignoring: {value}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:424 -msgid "ignoring action without \"from_jid\" value" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:426 -msgid "Confirmation needed for request from an entity not in roster" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:431 -msgid "Sender confirmed because she or he is explicitly expected" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:439 -msgid "Session refused for {from_jid}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:446 -msgid "Given path is not a directory !" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:450 -msgid "waiting for incoming file request" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:461 -msgid "download a file from URI" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:470 -msgid "destination file (DEFAULT: filename from URL)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:478 -msgid "URI of the file to retrieve" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:481 -msgid "File download started" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:484 -msgid "File downloaded successfully" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:487 -#, fuzzy, python-format -msgid "Error while downloading file: {}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_file.py:513 -msgid "file download cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:534 -msgid "upload a file" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:542 -msgid "encrypt file using AES-GCM" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:544 -msgid "file to upload" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:548 -msgid "jid of upload component (nothing to autodetect)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:553 -msgid "ignore invalide TLS certificate (/!\\ Dangerous /!\\)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:557 -msgid "File upload started" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:560 -msgid "File uploaded successfully" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:566 -msgid "URL to retrieve the file:" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:571 -msgid "Error while uploading file: {}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:593 -#, fuzzy, python-format -msgid "file {file_} doesn't exist !" -msgstr "Le fichier [%s] n'existe pas !" - -#: sat_frontends/jp/cmd_file.py:597 -msgid "{file_} is a dir! Can't upload a dir" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:633 -msgid "set affiliations for a shared file/directory" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:641 sat_frontends/jp/cmd_file.py:695 -#: sat_frontends/jp/cmd_file.py:747 sat_frontends/jp/cmd_file.py:801 -#: sat_frontends/jp/cmd_file.py:1002 -msgid "namespace of the repository" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:647 sat_frontends/jp/cmd_file.py:701 -#: sat_frontends/jp/cmd_file.py:753 sat_frontends/jp/cmd_file.py:807 -#: sat_frontends/jp/cmd_file.py:1007 -msgid "path to the repository" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:657 sat_frontends/jp/cmd_pubsub.py:453 -msgid "entity/affiliation couple(s)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:661 sat_frontends/jp/cmd_file.py:767 -msgid "jid of file sharing entity" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:687 -msgid "retrieve affiliations of a shared file/directory" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:705 sat_frontends/jp/cmd_file.py:811 -msgid "jid of sharing entity" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:729 -msgid "affiliations management" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:739 -msgid "set configuration for a shared file/directory" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:763 sat_frontends/jp/cmd_pubsub.py:282 -msgid "configuration field to set (required)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:793 -msgid "retrieve configuration of a shared file/directory" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:838 -#, fuzzy -msgid "file sharing node configuration" -msgstr "Demande de confirmation pour un transfer de fichier demandée" - -#: sat_frontends/jp/cmd_file.py:850 -msgid "retrieve files shared by an entity" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:859 -msgid "path to the directory containing the files" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:865 -msgid "jid of sharing entity (nothing to check our own jid)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:874 -msgid "unknown file type: {type}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:923 -msgid "share a file or directory" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:931 -msgid "virtual name to use (default: use directory/file name)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:941 -msgid "jid of contacts allowed to retrieve the files" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:946 -msgid "share publicly the file(s) (/!\\ *everybody* will be able to access them)" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:953 -msgid "path to a file or directory to share" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:978 -msgid "{path} shared under the name \"{name}\"" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:988 -msgid "send invitation for a shared repository" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:996 -#, fuzzy -msgid "name of the repository" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_file.py:1014 -msgid "type of the repository" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1019 -msgid "https URL of a image to use as thumbnail" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1023 -msgid "jid of the file sharing service hosting the repository" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1027 -#, fuzzy -msgid "jid of the person to invite" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_file.py:1035 -msgid "only http(s) links are allowed with --thumbnail" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1053 -msgid "invitation sent to {jid}" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1068 -msgid "files sharing management" -msgstr "" - -#: sat_frontends/jp/cmd_file.py:1077 -msgid "files sending/receiving/management" -msgstr "" - -#: sat_frontends/jp/cmd_forums.py:45 -msgid "edit forums" -msgstr "" - -#: sat_frontends/jp/cmd_forums.py:54 sat_frontends/jp/cmd_forums.py:123 -msgid "forum key (DEFAULT: default forums)" -msgstr "" - -#: sat_frontends/jp/cmd_forums.py:74 -msgid "forums have been edited" -msgstr "" - -#: sat_frontends/jp/cmd_forums.py:115 -msgid "get forums structure" -msgstr "" - -#: sat_frontends/jp/cmd_forums.py:168 sat_frontends/jp/cmd_pubsub.py:733 -#, fuzzy -msgid "no schema found" -msgstr "Aucun transport trouvé" - -#: sat_frontends/jp/cmd_forums.py:180 -msgid "Forums structure edition" -msgstr "" - -#: sat_frontends/jp/cmd_identity.py:37 -msgid "get identity data" -msgstr "" - -#: sat_frontends/jp/cmd_identity.py:45 -msgid "entity to check" -msgstr "" - -#: sat_frontends/jp/cmd_identity.py:68 -msgid "update identity data" -msgstr "" - -#: sat_frontends/jp/cmd_identity.py:77 -msgid "nicknames of the entity" -msgstr "" - -#: sat_frontends/jp/cmd_identity.py:101 -msgid "identity management" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:38 -msgid "service discovery" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:42 -msgid "entity to discover" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:49 -msgid "type of data to discover" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:51 -msgid "node to use" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:57 -#, fuzzy -msgid "ignore cache" -msgstr "fichier [%s] déjà en cache" - -#: sat_frontends/jp/cmd_info.py:69 -msgid "category" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:111 -msgid "node" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:116 -msgid "Features" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:118 -#, fuzzy -msgid "Identities" -msgstr "Petite" - -#: sat_frontends/jp/cmd_info.py:120 -msgid "Extensions" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:122 -msgid "Items" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:151 sat_frontends/jp/cmd_info.py:166 -msgid "error while doing discovery: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:190 -msgid "software version" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:193 sat_frontends/jp/cmd_info.py:258 -#, fuzzy -msgid "Entity to request" -msgstr "Demande de suppression de contact" - -#: sat_frontends/jp/cmd_info.py:201 -#, fuzzy, python-format -msgid "error while trying to get version: {e}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_info.py:207 -msgid "Software name: {name}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:209 -msgid "Software version: {version}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:211 -msgid "Operating System: {os}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:225 -#, fuzzy -msgid "running session" -msgstr "Lancement de l'application" - -#: sat_frontends/jp/cmd_info.py:243 -msgid "Error getting session infos: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:253 -msgid "devices of an entity" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:267 -msgid "Error getting devices infos: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_info.py:283 -msgid "Get various pieces of information on entities" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:61 -msgid "encoding of the input data" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:69 -msgid "standard input" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:77 -msgid "short option" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:85 -msgid "long option" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:93 -msgid "positional argument" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:101 -msgid "ignore value" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:107 -msgid "don't actually run commands but echo what would be launched" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:110 -msgid "log stdout to FILE" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:113 -msgid "log stderr to FILE" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:128 sat_frontends/jp/cmd_input.py:193 -msgid "arguments in input data and in arguments sequence don't match" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:155 sat_frontends/jp/cmd_input.py:207 -msgid "values: " -msgstr "" - -#: sat_frontends/jp/cmd_input.py:161 -msgid "**SKIPPING**\n" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:184 -msgid "Invalid argument, an option type is expected, got {type_}:{name}" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:199 -msgid "command {idx}" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:252 sat_frontends/primitivus/xmlui.py:461 -msgid "OK" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:254 -msgid "FAILED" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:274 -msgid "comma-separated values" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:283 -msgid "starting row (previous ones will be ignored)" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:291 -msgid "split value in several options" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:299 -msgid "action to do on empty value ({choices})" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:314 -msgid "--empty value must be one of {choices}" -msgstr "" - -#: sat_frontends/jp/cmd_input.py:349 -msgid "launch command with external input" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:38 -msgid "create and send an invitation" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:127 -msgid "you need to specify an email address to send email invitation" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:161 -#, fuzzy -msgid "get invitation data" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_invitation.py:165 -#: sat_frontends/jp/cmd_invitation.py:225 -#: sat_frontends/jp/cmd_invitation.py:289 -#, fuzzy -msgid "invitation UUID" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_invitation.py:170 -msgid "start profile session and retrieve jid" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:185 -msgid "can't get invitation data: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:198 -#, fuzzy -msgid "can't start session: {e}" -msgstr "Construction du jeu de Tarot" - -#: sat_frontends/jp/cmd_invitation.py:208 -msgid "can't retrieve jid: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:221 -#, fuzzy -msgid "delete guest account" -msgstr "Enregistrement d'un nouveau compte" - -#: sat_frontends/jp/cmd_invitation.py:233 -msgid "can't delete guest account: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:242 -msgid "modify existing invitation" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:299 -msgid "you can't set {arg_name} in both optional argument and extra" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:314 -msgid "invitations have been modified successfuly" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:328 -#, fuzzy -msgid "list invitations data" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_invitation.py:346 -msgid "return only invitations linked to this profile" -msgstr "" - -#: sat_frontends/jp/cmd_invitation.py:370 -msgid "invitation of user(s) without XMPP account" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:43 sat_frontends/jp/cmd_list.py:81 -#: sat_frontends/jp/cmd_list.py:150 sat_frontends/jp/cmd_merge_request.py:39 -#: sat_frontends/jp/cmd_merge_request.py:124 -#: sat_frontends/jp/cmd_merge_request.py:169 -msgid "auto" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:45 -msgid "get lists" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:82 -msgid "set a list item" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:94 -msgid "field(s) to set (required)" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:101 -msgid "update existing item instead of replacing it (DEFAULT: auto)" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:107 -msgid "id, URL of the item to update, or nothing for new item" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:151 -msgid "delete a list item" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:156 sat_frontends/jp/cmd_pubsub.py:884 -#: sat_frontends/jp/cmd_roster.py:135 -msgid "delete without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:159 sat_frontends/jp/cmd_pubsub.py:887 -#, fuzzy -msgid "notify deletion" -msgstr "Sélection du contrat" - -#: sat_frontends/jp/cmd_list.py:163 -msgid "id of the item to delete" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:169 -msgid "You need to specify a list item to delete" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:171 -#, fuzzy, python-format -msgid "Are you sure to delete list item {item_id} ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/jp/cmd_list.py:174 sat_frontends/jp/cmd_pubsub.py:897 -msgid "item deletion cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:184 sat_frontends/jp/cmd_pubsub.py:907 -#, fuzzy, python-format -msgid "can't delete item: {e}" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/jp/cmd_list.py:187 sat_frontends/jp/cmd_pubsub.py:910 -msgid "item {item} has been deleted" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:200 -msgid "import tickets from external software/dataset" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:225 -msgid "" -"specified field in import data will be put in dest field (default: use " -"same field name, or ignore if it doesn't exist)" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:235 -msgid "PubSub service where the items must be uploaded (default: server)" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:242 -msgid "PubSub node where the items must be uploaded (default: tickets' defaults)" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:257 -msgid "Tickets upload started" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:260 -msgid "Tickets uploaded successfully" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:264 -#, fuzzy, python-format -msgid "Error while uploading tickets: {error_msg}" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/jp/cmd_list.py:319 -msgid "" -"fields_map must be specified either preencoded in --option or using " -"--map, but not both at the same time" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:337 -msgid "Error while trying to import tickets: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_list.py:350 -msgid "pubsub lists handling" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:40 -msgid "publish or update a merge request" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:48 -msgid "id or URL of the request to update, or nothing for a new one" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:55 -#: sat_frontends/jp/cmd_merge_request.py:179 -msgid "path of the repository (DEFAULT: current directory)" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:61 -msgid "publish merge request without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:68 -msgid "labels to categorize your request" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:75 -msgid "" -"You are going to publish your changes to service [{service}], are you " -"sure ?" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:80 -msgid "merge request publication cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:105 -msgid "Merge request published at {published_id}" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:110 -msgid "Merge request published" -msgstr "" - -#: sat_frontends/jp/cmd_merge_request.py:125 -#, fuzzy -msgid "get a merge request" -msgstr "Demande de changement de statut" - -#: sat_frontends/jp/cmd_merge_request.py:170 -#, fuzzy -msgid "import a merge request" -msgstr "Demande de changement de statut" - -#: sat_frontends/jp/cmd_merge_request.py:209 -msgid "merge-request management" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:34 -#, fuzzy -msgid "send a message to a contact" -msgstr "Attend qu'un fichier soit envoyé par un contact" - -#: sat_frontends/jp/cmd_message.py:38 -msgid "language of the message" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:44 -#, fuzzy -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." - -#: sat_frontends/jp/cmd_message.py:53 -#, fuzzy -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 ;))" - -#: sat_frontends/jp/cmd_message.py:60 -msgid "subject of the message" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:63 -msgid "language of subject" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:70 -msgid "type of the message" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:73 -msgid "encrypt message using given algorithm" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:79 -msgid "XHTML body" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:80 -msgid "rich body" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:195 -msgid "query archives using MAM" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:203 -msgid "start fetching archive from this date (default: from the beginning)" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:207 -msgid "end fetching archive after this date (default: no limit)" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:210 -msgid "retrieve only archives with this jid" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:213 -msgid "maximum number of items to retrieve, using RSM (default: 20))" -msgstr "" - -#: sat_frontends/jp/cmd_message.py:276 -msgid "messages handling" -msgstr "" - -#: sat_frontends/jp/cmd_param.py:32 -#, fuzzy -msgid "get a parameter value" -msgstr "Impossible de charger les paramètres !" - -#: sat_frontends/jp/cmd_param.py:37 sat_frontends/jp/cmd_param.py:94 -#, fuzzy -msgid "category of the parameter" -msgstr "Impossible de charger les paramètres !" - -#: sat_frontends/jp/cmd_param.py:39 sat_frontends/jp/cmd_param.py:95 -#: sat_frontends/jp/cmd_param.py:96 -#, fuzzy -msgid "name of the parameter" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_param.py:45 -msgid "name of the attribute to get" -msgstr "" - -#: sat_frontends/jp/cmd_param.py:48 sat_frontends/jp/cmd_param.py:98 -msgid "security limit" -msgstr "" - -#: sat_frontends/jp/cmd_param.py:62 -#, fuzzy -msgid "can't find requested parameters: {e}" -msgstr "Impossible de charger les paramètres !" - -#: sat_frontends/jp/cmd_param.py:79 -#, fuzzy -msgid "can't find requested parameter: {e}" -msgstr "Impossible de charger les paramètres !" - -#: sat_frontends/jp/cmd_param.py:90 -msgid "set a parameter value" -msgstr "" - -#: sat_frontends/jp/cmd_param.py:111 -#, fuzzy -msgid "can't set requested parameter: {e}" -msgstr "Mauvais nom de profile" - -#: sat_frontends/jp/cmd_param.py:125 -#, fuzzy, python-format -msgid "save parameters template to xml file" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat_frontends/jp/cmd_param.py:129 -msgid "output file" -msgstr "" - -#: sat_frontends/jp/cmd_param.py:136 -#, fuzzy, python-format -msgid "can't save parameters to file: {e}" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat_frontends/jp/cmd_param.py:140 -#, fuzzy, python-format -msgid "parameters saved to file {filename}" -msgstr "Échec de la désinscription: %s" - -#: sat_frontends/jp/cmd_param.py:155 -#, fuzzy, python-format -msgid "load parameters template from xml file" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat_frontends/jp/cmd_param.py:159 -#, fuzzy -msgid "input file" -msgstr "Envoi un fichier" - -#: sat_frontends/jp/cmd_param.py:166 -#, fuzzy, python-format -msgid "can't load parameters from file: {e}" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat_frontends/jp/cmd_param.py:170 -#, fuzzy, python-format -msgid "parameters loaded from file {filename}" -msgstr "Échec de la désinscription: %s" - -#: sat_frontends/jp/cmd_param.py:182 -#, fuzzy -msgid "Save/load parameters template" -msgstr "Impossible de charger le modèle des paramètres !" - -#: sat_frontends/jp/cmd_ping.py:29 -msgid "ping XMPP entity" -msgstr "" - -#: sat_frontends/jp/cmd_ping.py:32 -msgid "jid to ping" -msgstr "" - -#: sat_frontends/jp/cmd_ping.py:34 -msgid "output delay only (in s)" -msgstr "" - -#: sat_frontends/jp/cmd_ping.py:41 -msgid "can't do the ping: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pipe.py:38 -msgid "send a pipe a stream" -msgstr "" - -#: sat_frontends/jp/cmd_pipe.py:97 -#, fuzzy -msgid "receive a pipe stream" -msgstr "Lancement du flux" - -#: sat_frontends/jp/cmd_pipe.py:104 -msgid "Jids accepted (none means \"accept everything\")" -msgstr "" - -#: sat_frontends/jp/cmd_pipe.py:159 -msgid "stream piping through XMPP" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:33 -#, fuzzy -msgid "The name of the profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_profile.py:51 -msgid "You need to use either --connect or --start-session" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:78 -#, fuzzy -msgid "the name of the profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_profile.py:81 -msgid "the password of the profile" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:83 sat_frontends/jp/cmd_profile.py:238 -#, fuzzy -msgid "the jid of the profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_profile.py:86 -msgid "the password of the XMPP account (use profile password if not specified)" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:93 sat_frontends/jp/cmd_profile.py:247 -msgid "connect this profile automatically when backend starts" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:97 -msgid "set to component import name (entry point) if this is a component" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:154 -msgid "delete profile without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:174 -#, fuzzy -msgid "get information about a profile" -msgstr "Demande de contacts pour un profile inexistant" - -#: sat_frontends/jp/cmd_profile.py:180 -msgid "show the XMPP password IN CLEAR TEXT" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:184 -#, fuzzy -msgid "XMPP password" -msgstr "Mot de passe:" - -#: sat_frontends/jp/cmd_profile.py:185 -msgid "autoconnect (backend)" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:209 -msgid "get clients profiles only" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:229 -msgid "modify an existing profile" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:234 -#, fuzzy -msgid "change the password of the profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_profile.py:237 -msgid "disable profile password (dangerous!)" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:240 -msgid "change the password of the XMPP account" -msgstr "" - -#: sat_frontends/jp/cmd_profile.py:243 -#, fuzzy -msgid "set as default profile" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/jp/cmd_profile.py:280 -#, fuzzy -msgid "profile commands" -msgstr "Mauvais nom de profile" - -#: sat_frontends/jp/cmd_pubsub.py:59 -#, fuzzy -msgid "retrieve node configuration" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_pubsub.py:68 -msgid "data key to filter" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:116 -#, fuzzy -msgid "create a node" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/jp/cmd_pubsub.py:135 sat_frontends/jp/cmd_pubsub.py:288 -msgid "don't prepend \"pubsub#\" prefix to field names" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:158 -msgid "can't create node: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:162 -#, fuzzy -msgid "node created successfully: " -msgstr "Inscription réussie" - -#: sat_frontends/jp/cmd_pubsub.py:176 -msgid "purge a node (i.e. remove all items from it)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:184 -#, fuzzy -msgid "purge node without confirmation" -msgstr "désinscription confirmée pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:190 -msgid "" -"Are you sure to purge PEP node [{node}]? This will delete ALL items from " -"it!" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:195 -msgid "" -"Are you sure to delete node [{node}] on service [{service}]? This will " -"delete ALL items from it!" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:199 -msgid "node purge cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:208 -msgid "can't purge node: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:211 -msgid "node [{node}] purged successfully" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:223 -#, fuzzy -msgid "delete a node" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_pubsub.py:231 -msgid "delete node without confirmation" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:237 -#, fuzzy, python-format -msgid "Are you sure to delete PEP node [{node}] ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/jp/cmd_pubsub.py:241 -#, fuzzy, python-format -msgid "Are you sure to delete node [{node}] on service [{service}]?" -msgstr "" -"Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur" -" %(server)s ?" - -#: sat_frontends/jp/cmd_pubsub.py:244 -msgid "node deletion cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:256 -msgid "node [{node}] deleted successfully" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:270 -#, fuzzy -msgid "set node configuration" -msgstr "Connexion..." - -#: sat_frontends/jp/cmd_pubsub.py:309 -#, fuzzy -msgid "node configuration successful" -msgstr "Inscription réussie" - -#: sat_frontends/jp/cmd_pubsub.py:320 -msgid "import raw XML to a node" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:327 sat_frontends/jp/cmd_pubsub.py:1608 -msgid "do a pubsub admin request, needed to change publisher" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:332 -msgid "" -"path to the XML file with data to import. The file must contain whole XML" -" of each item to import." -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:359 -msgid "You are not using list of pubsub items, we can't import this file" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:370 -msgid "Items are imported without using admin mode, publisher can't be changed" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:391 -msgid "items published with id(s) {items_ids}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:396 sat_frontends/jp/cmd_pubsub.py:1641 -msgid "items published" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:409 -msgid "retrieve node affiliations (for node owner)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:439 -msgid "set affiliations (for node owner)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:469 -msgid "affiliations have been set" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:481 -msgid "set or retrieve node affiliations" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:494 -msgid "retrieve node subscriptions (for node owner)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:532 -#, fuzzy, python-format -msgid "subscription must be one of {}" -msgstr "inscription approuvée pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:548 -msgid "set/modify subscriptions (for node owner)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:563 -msgid "entity/subscription couple(s)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:578 -#, fuzzy, python-format -msgid "subscriptions have been set" -msgstr "inscription approuvée pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:590 -msgid "get or modify node subscriptions" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:603 -msgid "set/replace a schema" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:607 -msgid "schema to set (must be XML)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:621 sat_frontends/jp/cmd_pubsub.py:656 -msgid "schema has been set" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:637 -msgid "edit a schema" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:709 -msgid "get schema" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:742 -msgid "data schema manipulation" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:761 -msgid "node handling" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:774 -msgid "publish a new item or update an existing one" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:783 -msgid "id, URL of the item to update, keyword, or nothing for new item" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:805 -#, fuzzy, python-format -msgid "can't send item: {e}" -msgstr "message reçu de: %s" - -#: sat_frontends/jp/cmd_pubsub.py:827 -msgid "get pubsub item(s)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:835 -#, fuzzy -msgid "subscription id" -msgstr "demande d'inscription pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:879 -#, fuzzy -msgid "delete an item" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_pubsub.py:892 -msgid "You need to specify an item to delete" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:894 -#, fuzzy, python-format -msgid "Are you sure to delete item {item_id} ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/jp/cmd_pubsub.py:924 -msgid "edit an existing or new pubsub item" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:968 -msgid "Item has not payload" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:992 -msgid "rename a pubsub item" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1024 -msgid "subscribe to a node" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1039 -msgid "can't subscribe to node: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1042 -#, fuzzy -msgid "subscription done" -msgstr "demande d'inscription pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:1044 -msgid "subscription id: {sub_id}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1059 -msgid "unsubscribe from a node" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1073 -msgid "can't unsubscribe from node: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1076 -#, fuzzy, python-format -msgid "subscription removed" -msgstr "inscription approuvée pour [%s]" - -#: sat_frontends/jp/cmd_pubsub.py:1088 -msgid "retrieve all subscriptions on a service" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1102 -msgid "can't retrieve subscriptions: {e}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1117 -msgid "retrieve all affiliations on a service" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1160 -msgid "search items corresponding to filters" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1185 -msgid "maximum depth of recursion (will search linked nodes if > 0, DEFAULT: 0)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1197 -msgid "maximum number of items to get per node ({} to get all items, DEFAULT: 30)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1207 -msgid "namespace to use for xpath" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1216 -msgid "filters" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1217 -msgid "only items corresponding to following filters will be kept" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1226 -msgid "full text filter, item must contain this string (XML included)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1235 -msgid "like --text but using a regular expression" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1244 -msgid "filter items which has elements matching this xpath" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1253 -msgid "" -"Python expression which much return a bool (True to keep item, False to " -"reject it). \"item\" is raw text item, \"item_xml\" is lxml's " -"etree.Element" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1266 -msgid "filters flags" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1267 -msgid "filters modifiers (change behaviour of following filters)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1278 -msgid "(don't) ignore case in following filters (DEFAULT: case sensitive)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1289 -msgid "(don't) invert effect of following filters (DEFAULT: don't invert)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1300 -msgid "(don't) use DOTALL option for regex (DEFAULT: don't use)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1311 -msgid "keep only the matching part of the item" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1320 -msgid "action to do on found items (DEFAULT: print)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1383 -msgid "" -"item doesn't looks like XML, you have probably used --only-matching " -"somewhere before and we have no more XML" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1420 -msgid "--only-matching used with fixed --text string, are you sure?" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1441 -msgid "can't use xpath: {reason}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1476 -#, fuzzy, python-format -msgid "unknown filter type {type}" -msgstr "Type d'action inconnu" - -#: sat_frontends/jp/cmd_pubsub.py:1534 -msgid "executed command failed with exit code {ret}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1566 -msgid "Command can only be used with {actions} actions" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1572 -msgid "you need to specify a command to execute" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1575 -msgid "empty node is not handled yet" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1596 -msgid "modify items of a node using an external command/script" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1603 -msgid "apply transformation (DEFAULT: do a dry run)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1614 -msgid "if command return a non zero exit code, ignore the item and continue" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1622 -msgid "get all items by looping over all pages using RSM" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1626 -msgid "" -"path to the command to use. Will be called repetitivly with an item as " -"input. Output (full item XML) will be used as new one. Return \"DELETE\" " -"string to delete the item, and \"SKIP\" to ignore it" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1636 -msgid "items published with ids {item_ids}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1659 -msgid "Can't retrieve all items, RSM metadata not available" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1664 -msgid "Can't retrieve all items, bad RSM metadata: {msg}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1670 -msgid "All items transformed" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1674 -msgid "Retrieving next page ({page_idx}/{page_total})" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1712 -msgid "Duplicate found on item {item_id}, we have probably handled all items." -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1749 -msgid "Deleting item {item_id}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1766 -msgid "Skipping item {item_id}" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1860 sat_frontends/jp/cmd_uri.py:53 -msgid "build URI" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1868 -msgid "profile (used when no server is specified)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1908 -msgid "create a Pubsub hook" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1917 -msgid "hook type" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1923 -msgid "make hook persistent across restarts" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1927 -msgid "argument of the hook (depend of the type)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1936 -msgid "{path} is not a file" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1965 -msgid "delete a Pubsub hook" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1974 -msgid "hook type to remove, empty to remove all (DEFAULT: remove all)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:1981 -msgid "argument of the hook to remove, empty to remove all (DEFAULT: remove all)" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:2001 -msgid "{nb_deleted} hook(s) have been deleted" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:2013 -#, fuzzy -msgid "list hooks of a profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_pubsub.py:2029 -#, fuzzy -msgid "No hook found." -msgstr "Aucune donnée trouvée" - -#: sat_frontends/jp/cmd_pubsub.py:2043 -msgid "trigger action on Pubsub notifications" -msgstr "" - -#: sat_frontends/jp/cmd_pubsub.py:2067 -msgid "PubSub nodes/items management" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:36 -msgid "retrieve the roster entities" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:89 -msgid "set metadata for a roster entity" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:93 -msgid "name to use for this entity" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:96 -msgid "groups for this entity" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:99 -msgid "replace all metadata instead of adding them" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:101 sat_frontends/jp/cmd_roster.py:138 -#, fuzzy -msgid "jid of the roster entity" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/jp/cmd_roster.py:131 -#, fuzzy, python-format -msgid "remove an entity from roster" -msgstr "supppression du contact %s" - -#: sat_frontends/jp/cmd_roster.py:142 -#, fuzzy, python-format -msgid "Are you sure to delete {entity} fril your roster?" -msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" - -#: sat_frontends/jp/cmd_roster.py:145 -msgid "entity deletion cancelled" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:158 -msgid "Show statistics about a roster" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:226 -msgid "purge the roster from its contacts with no subscription" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:231 -#, fuzzy, python-format -msgid "also purge contacts with no 'from' subscription" -msgstr "Le contact %s a refusé votre inscription" - -#: sat_frontends/jp/cmd_roster.py:234 -#, fuzzy, python-format -msgid "also purge contacts with no 'to' subscription" -msgstr "Le contact %s a refusé votre inscription" - -#: sat_frontends/jp/cmd_roster.py:306 -msgid "do a full resynchronisation of roster with server" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:318 -msgid "Roster resynchronized" -msgstr "" - -#: sat_frontends/jp/cmd_roster.py:327 -msgid "Manage an entity's roster" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:33 -msgid "" -"Welcome to {app_name} shell, the Salut à Toi shell !\n" -"\n" -"This enrironment helps you using several {app_name} commands with similar" -" parameters.\n" -"\n" -"To quit, just enter \"quit\" or press C-d.\n" -"Enter \"help\" or \"?\" to know what to do\n" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:48 -msgid "launch jp in shell (REPL) mode" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:63 -msgid "bad command path" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:104 -msgid "COMMAND {external}=> {args}" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:105 -msgid "(external) " -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:149 -#, fuzzy -msgid "Shell commands:" -msgstr "Mauvais nom de profile" - -#: sat_frontends/jp/cmd_shell.py:152 -#, fuzzy -msgid "Action commands:" -msgstr "Mauvais nom de profile" - -#: sat_frontends/jp/cmd_shell.py:172 -msgid "verbose mode is {status}" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:173 -msgid "ENABLED" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:173 -msgid "DISABLED" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:214 -msgid "arg profile={profile} (logged profile)" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:236 -msgid "no argument in USE" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:238 -msgid "arguments in USE:" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:274 -msgid "argument {name} not found" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:280 -msgid "argument {name} removed" -msgstr "" - -#: sat_frontends/jp/cmd_shell.py:288 -msgid "good bye!" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:37 -msgid "parse URI" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:42 -msgid "XMPP URI to parse" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:57 -msgid "URI type" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:58 -msgid "URI path" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:66 -msgid "URI fields" -msgstr "" - -#: sat_frontends/jp/cmd_uri.py:80 -msgid "XMPP URI parsing/generation" -msgstr "" - -#: sat_frontends/jp/common.py:437 -msgid "no item found at all, we create a new one" -msgstr "" - -#: sat_frontends/jp/common.py:440 -msgid "item \"{item}\" not found, we create a new item withthis id" -msgstr "" - -#: sat_frontends/jp/common.py:458 -msgid "item \"{item}\" found, we edit it" -msgstr "" - -#: sat_frontends/jp/common.py:785 -msgid "No {key} URI specified for this project, please specify service and node" -msgstr "" - -#: sat_frontends/jp/common.py:821 -msgid "Invalid URI found: {uri}" -msgstr "" - -#: sat_frontends/jp/loops.py:28 -msgid "User interruption: good bye" -msgstr "Interrompu par l'utilisateur: au revoir" - -#: sat_frontends/jp/output_template.py:53 -msgid "Can't find requested template: {template_path}" -msgstr "" - -#: sat_frontends/jp/output_template.py:74 -msgid "" -"no default template set for this command, you need to specify a template " -"using --oo template=[path/to/template.html]" -msgstr "" - -#: sat_frontends/jp/output_template.py:89 -msgid "Can't parse template, please check its syntax" -msgstr "" - -#: sat_frontends/jp/output_template.py:109 -msgid "" -"Browser opening requested.\n" -"Temporary files are put in the following directory, you'll have to delete" -" it yourself once finished viewing: {}" -msgstr "" - -#: sat_frontends/jp/output_xml.py:56 -msgid "" -"Pygments is not available, syntax highlighting is not possible. Please " -"install if from http://pygments.org or with pip install pygments" -msgstr "" - -#: sat_frontends/jp/xml_tools.py:50 -msgid "Can't parse the payload XML in input: {msg}" -msgstr "" - -#: sat_frontends/jp/xml_tools.py:62 -msgid " can only have one child element (the payload)" -msgstr "" - -#: sat_frontends/jp/xmlui_manager.py:224 -msgid "(enter: {value})" -msgstr "" - -#: sat_frontends/jp/xmlui_manager.py:318 -msgid "your choice (0-{limit_max}): " -msgstr "" - -#: sat_frontends/jp/xmlui_manager.py:348 -msgid "your choice (0,1): " -msgstr "" - -#: sat_frontends/primitivus/base.py:90 -#, fuzzy, python-format -msgid "Error while sending message ({})" -msgstr "Erreur en tentant de rejoindre le salon" - -#: sat_frontends/primitivus/base.py:135 -msgid "Please specify the globbing pattern to search for" -msgstr "" - -#: sat_frontends/primitivus/base.py:377 -#, fuzzy -msgid "Configuration Error" -msgstr "Connexion..." - -#: sat_frontends/primitivus/base.py:377 -msgid "" -"Something went wrong while reading the configuration, please check " -":messages" -msgstr "" - -#: sat_frontends/primitivus/base.py:504 -msgid "Pleeeeasse, I can't even breathe !" -msgstr "Pitiééééééééé, je ne peux même pas respirer !" - -#: sat_frontends/primitivus/base.py:534 -#: sat_frontends/primitivus/profile_manager.py:64 -#, fuzzy -msgid "Connect" -msgstr "Connexion..." - -#: sat_frontends/primitivus/base.py:536 -#, fuzzy -msgid "Parameters" -msgstr "&Paramètres" - -#: sat_frontends/primitivus/base.py:537 sat_frontends/primitivus/base.py:851 -msgid "About" -msgstr "À propos" - -#: sat_frontends/primitivus/base.py:538 -#, fuzzy -msgid "Exit" -msgstr "Quitter" - -#: sat_frontends/primitivus/base.py:542 -msgid "Join room" -msgstr "Rejoindre un salon" - -#: sat_frontends/primitivus/base.py:547 -#, fuzzy -msgid "Main menu" -msgstr "Construction des menus" - -#: sat_frontends/primitivus/base.py:658 -msgid "{app}: a new event has just happened{entity}" -msgstr "" - -#: sat_frontends/primitivus/base.py:736 -#, fuzzy -msgid "Chat menu" -msgstr "Construction des menus" - -#: sat_frontends/primitivus/base.py:790 -#, fuzzy -msgid "Unmanaged action" -msgstr "Tab inconnu" - -#: sat_frontends/primitivus/base.py:801 -#, fuzzy -msgid "unkown" -msgstr "Messagerie inconnue" - -#: sat_frontends/primitivus/base.py:831 -#, fuzzy, python-format -msgid "Can't get parameters (%s)" -msgstr "Impossible de charger les paramètres !" - -#: sat_frontends/primitivus/base.py:846 -msgid "Entering a MUC room" -msgstr "Entrée dans le salon MUC" - -#: sat_frontends/primitivus/base.py:846 -#, fuzzy -msgid "Please enter MUC's JID" -msgstr "Veuillez entrer le JID de votre nouveau contact" - -#: sat_frontends/primitivus/chat.py:40 -msgid "{} occupants" -msgstr "" - -#: sat_frontends/primitivus/chat.py:381 -msgid "Game" -msgstr "Jeu" - -#: sat_frontends/primitivus/chat.py:502 -msgid "You have been mentioned by {nick} in {room}" -msgstr "" - -#: sat_frontends/primitivus/chat.py:513 -msgid "{entity} is talking to you" -msgstr "" - -#: sat_frontends/primitivus/chat.py:612 -msgid "Results for searching the globbing pattern: {}" -msgstr "" - -#: sat_frontends/primitivus/chat.py:618 -msgid "Type ':history ' to reset the chat history" -msgstr "" - -#: sat_frontends/primitivus/chat.py:652 -#, python-format -msgid "Primitivus: %s is talking to you" -msgstr "" - -#: sat_frontends/primitivus/chat.py:656 -#, fuzzy, python-format -msgid "Primitivus: %(user)s mentioned you in room '%(room)s'" -msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" - -#: sat_frontends/primitivus/chat.py:666 -#, fuzzy -msgid "Can't start game" -msgstr "Construction du jeu de Tarot" - -#: sat_frontends/primitivus/chat.py:667 -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" - -#: sat_frontends/primitivus/chat.py:698 -msgid "Change title" -msgstr "" - -#: sat_frontends/primitivus/chat.py:699 -#, fuzzy -msgid "Enter the new title" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/primitivus/game_tarot.py:290 -msgid "Please choose your contrat" -msgstr "Veuillez choisir votre contrat" - -#: sat_frontends/primitivus/game_tarot.py:311 -msgid "You win \\o/" -msgstr "Victoire \\o/" - -#: sat_frontends/primitivus/game_tarot.py:311 -msgid "You loose :(" -msgstr "Vous perdez :(" - -#: sat_frontends/primitivus/game_tarot.py:331 -msgid "Cards played are invalid !" -msgstr "Les cartes jouées sont invalides !" - -#: sat_frontends/primitivus/game_tarot.py:369 -msgid "Do you put these cards in chien ?" -msgstr "Voulez-vous placer ces cartes au chien ?" - -#: sat_frontends/primitivus/profile_manager.py:36 -#, fuzzy -msgid "Login:" -msgstr "Identifiant" - -#: sat_frontends/primitivus/profile_manager.py:37 -msgid "Password:" -msgstr "Mot de passe:" - -#: sat_frontends/primitivus/profile_manager.py:48 -msgid "New" -msgstr "Nouveau" - -#: sat_frontends/primitivus/profile_manager.py:49 -msgid "Delete" -msgstr "Suppression" - -#: sat_frontends/primitivus/profile_manager.py:81 -#, fuzzy -msgid "Profile Manager" -msgstr "Mauvais nom de profile" - -#: sat_frontends/primitivus/profile_manager.py:142 -msgid "Can't create profile" -msgstr "" - -#: sat_frontends/primitivus/profile_manager.py:150 -#, fuzzy -msgid "New profile" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/primitivus/profile_manager.py:151 -#, fuzzy -msgid "Please enter a new profile name" -msgstr "Veuillez entrer le nom du nouveau profile" - -#: sat_frontends/primitivus/profile_manager.py:160 -#, fuzzy, python-format -msgid "Are you sure you want to delete the profile {} ?" -msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" - -#: sat_frontends/primitivus/progress.py:37 -msgid "Clear progress list" -msgstr "Effacer la liste" - -#: sat_frontends/primitivus/status.py:57 -msgid "Set your presence" -msgstr "" - -#: sat_frontends/primitivus/status.py:67 -msgid "Set your status" -msgstr "" - -#: sat_frontends/primitivus/status.py:68 -msgid "New status" -msgstr "" - -#: sat_frontends/primitivus/xmlui.py:78 -#, fuzzy -msgid "Unknown div_char" -msgstr "Type d'action inconnu" - -#: sat_frontends/primitivus/xmlui.py:456 -msgid "Submit" -msgstr "Envoyer" - -#: sat_frontends/primitivus/xmlui.py:458 sat_frontends/primitivus/xmlui.py:473 -msgid "Cancel" -msgstr "Annuler" - -#: sat_frontends/quick_frontend/constants.py:31 -msgid "Away from keyboard" -msgstr "" - -#: sat_frontends/quick_frontend/constants.py:33 -msgid "Extended away" -msgstr "" - -#: sat_frontends/quick_frontend/quick_app.py:85 -msgid "Error while trying to get autodisconnect param, ignoring: {}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_app.py:200 -#, fuzzy -msgid "Can't get profile parameter: {msg}" -msgstr "Mauvais nom de profile" - -#: sat_frontends/quick_frontend/quick_app.py:324 -msgid "Can't get namespaces map: {msg}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_app.py:330 -msgid "Can't retrieve encryption plugins: {msg}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_app.py:376 -msgid "Error while initialising bridge: {}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_app.py:662 -#, fuzzy, python-format -msgid "Can't connect profile [%s]" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/quick_frontend/quick_app.py:723 -#, fuzzy -msgid "Connected" -msgstr "Connexion..." - -#: sat_frontends/quick_frontend/quick_app.py:739 -#, fuzzy -msgid "Disconnected" -msgstr "Déconnexion..." - -#: sat_frontends/quick_frontend/quick_app.py:1154 -#, fuzzy, python-format -msgid "The contact {contact} has accepted your subscription" -msgstr "Le contact %s a accepté votre inscription" - -#: sat_frontends/quick_frontend/quick_app.py:1157 -#: sat_frontends/quick_frontend/quick_app.py:1176 -#, fuzzy -msgid "Subscription confirmation" -msgstr "désinscription confirmée pour [%s]" - -#: sat_frontends/quick_frontend/quick_app.py:1162 -#, fuzzy, python-format -msgid "The contact {contact} has refused your subscription" -msgstr "Le contact %s a refusé votre inscription" - -#: sat_frontends/quick_frontend/quick_app.py:1165 -#, fuzzy -msgid "Subscription refusal" -msgstr "demande d'inscription pour [%s]" - -#: sat_frontends/quick_frontend/quick_app.py:1172 -#, fuzzy, python-format -msgid "" -"The contact {contact} 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 ?" - -#: sat_frontends/quick_frontend/quick_app.py:1229 -#, python-format -msgid "param update: [%(namespace)s] %(name)s = %(value)s" -msgstr "Le paramètre [%(namespace)s] %(name)s vaut désormais %(value)s" - -#: sat_frontends/quick_frontend/quick_app.py:1233 -#, python-format -msgid "Changing JID to %s" -msgstr "Changement du JID pour %s" - -#: sat_frontends/quick_frontend/quick_chat.py:624 -#, fuzzy -msgid "now we print the history" -msgstr "Maintenant on affiche l'historique" - -#: sat_frontends/quick_frontend/quick_chat.py:626 -#, fuzzy -msgid " ({} messages)" -msgstr "Messages" - -#: sat_frontends/quick_frontend/quick_chat.py:683 -#, fuzzy -msgid "Can't get history: {}" -msgstr "Impossible de charger l'historique !" - -#: sat_frontends/quick_frontend/quick_chat.py:705 -msgid "Can't get encryption state: {reason}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_chat.py:775 -msgid "message encryption started with {target} using {encryption}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_chat.py:780 -msgid "message encryption stopped with {target} (was using {encryption})" -msgstr "" - -#: sat_frontends/quick_frontend/quick_chat.py:833 -msgid "<= {nick} has left the room ({count})" -msgstr "" - -#: sat_frontends/quick_frontend/quick_chat.py:837 -msgid "<=> {nick} re-entered the room ({count})" -msgstr "" - -#: sat_frontends/quick_frontend/quick_contact_list.py:611 -#, fuzzy -msgid "Trying to delete an unknow entity [{}]" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat_frontends/quick_frontend/quick_contact_list.py:664 -msgid "received presence from entity without resource: {}" -msgstr "" - -#: sat_frontends/quick_frontend/quick_contact_management.py:73 -#, fuzzy -msgid "Trying to get attribute for an unknown contact" -msgstr "Tentative d'assigner un paramètre à un profile inconnu" - -#: sat_frontends/quick_frontend/quick_contact_management.py:89 -msgid "INTERNAL ERROR: Key log.error" -msgstr "" - -#: sat_frontends/quick_frontend/quick_contact_management.py:101 -#, fuzzy, python-format -msgid "Trying to update an unknown contact: %s" -msgstr "Tentative d'accès à un profile inconnu" - -#: sat_frontends/quick_frontend/quick_games.py:84 -msgid "" -"A {game} activity between {players} has been started, but you couldn't " -"take part because your client doesn't support it." -msgstr "" - -#: sat_frontends/quick_frontend/quick_games.py:87 -msgid "{game} Game" -msgstr "" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:116 -#, fuzzy, python-format -msgid "Trying to plug an unknown profile key ({})" -msgstr "Tentative d'appel d'un profile inconnue" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:118 -#, fuzzy -msgid "Profile plugging in error" -msgstr "Mauvais nom de profile" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:133 -#, fuzzy -msgid "Can't get profile parameter" -msgstr "Mauvais nom de profile" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:144 -#, fuzzy -msgid "A profile with this name already exists" -msgstr "Ce nom de profile existe déjà" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:146 -msgid "Profile creation cancelled by backend" -msgstr "" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:148 -#, fuzzy -msgid "You profile name is not valid" -msgstr "Ce profile n'est pas utilisé" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:152 -#, fuzzy -msgid "Can't create profile ({})" -msgstr "Vous essayer de connecter un profile qui n'existe pas" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:172 -msgid "You can't connect manually and automatically at the same time" -msgstr "" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:180 -msgid "No profile selected" -msgstr "Aucun profile sélectionné" - -#: sat_frontends/quick_frontend/quick_profile_manager.py:181 -#, fuzzy -msgid "You need to create and select at least one profile before connecting" -msgstr "" -"Vous devez sélectionner un profile ou en créer un nouveau avant de vous " -"connecter." - -#: sat_frontends/quick_frontend/quick_utils.py:40 -#, fuzzy -msgid "" -"\n" -" %prog [options]\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" -" " - -#: sat_frontends/quick_frontend/quick_utils.py:49 -msgid "Select the profile to use" -msgstr "Veuillez sélectionner le profile à utiliser" - -#: sat_frontends/tools/xmlui.py:233 -msgid "Nothing to submit" -msgstr "" - -#: sat_frontends/tools/xmlui.py:449 -msgid "XMLUI can have only one main container" -msgstr "" - -#: sat_frontends/tools/xmlui.py:514 -#, fuzzy, python-format -msgid "Unknown container [%s], using default one" -msgstr "Disposition inconnue, utilisation de celle par defaut" - -#: sat_frontends/tools/xmlui.py:527 -msgid "Internal Error, container has not _xmluiAppend method" -msgstr "" - -#: sat_frontends/tools/xmlui.py:674 -#, fuzzy, python-format -msgid "FIXME FIXME FIXME: widget type [%s] is not implemented" -msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" - -#: sat_frontends/tools/xmlui.py:678 -#, fuzzy, python-format -msgid "FIXME FIXME FIXME: type [%s] is not implemented" -msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" - -#: sat_frontends/tools/xmlui.py:696 -#, python-format -msgid "No change listener on [%s]" -msgstr "" - -#: sat_frontends/tools/xmlui.py:722 -#, fuzzy, python-format -msgid "Unknown tag [%s]" -msgstr "Type d'action inconnu" - -#: sat_frontends/tools/xmlui.py:780 -#, fuzzy -msgid "No callback_id found" -msgstr "Aucun transport trouvé" - -#: sat_frontends/tools/xmlui.py:813 -#, python-format -msgid "FIXME: XMLUI internal action [%s] is not implemented" -msgstr "" - -#: sat_frontends/tools/xmlui.py:909 sat_frontends/tools/xmlui.py:921 -#: sat_frontends/tools/xmlui.py:971 sat_frontends/tools/xmlui.py:983 -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" - -#: sat_frontends/tools/xmlui.py:915 sat_frontends/tools/xmlui.py:977 -msgid "Cancelling form" -msgstr "Annulation du formulaire" - -#: sat_frontends/tools/xmlui.py:1096 -msgid "XMLUI class already registered for {type_}, ignoring" -msgstr "" - -#: sat_frontends/tools/xmlui.py:1135 -msgid "You must register classes with registerClass before creating a XMLUI" -msgstr "" - diff -r d10748475025 -r 4b842c1fb686 libervia/backend/VERSION --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/VERSION Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1 @@ +0.9.0D diff -r d10748475025 -r 4b842c1fb686 libervia/backend/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/__init__.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import os.path +from sat_tmp import wokkel + +version_file = os.path.join(os.path.dirname(__file__), "VERSION") +with open(version_file) as f: + __version__ = f.read().strip() + +if not wokkel.installed: + wokkel.install() diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/base_constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/base_constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,364 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""base constructor class""" + +from libervia.backend.bridge.bridge_constructor.constants import Const as C +from configparser import NoOptionError +import sys +import os +import os.path +import re +from importlib import import_module + + +class ParseError(Exception): + # Used when the signature parsing is going wrong (invalid signature ?) + pass + + +class Constructor(object): + NAME = None # used in arguments parsing, filename will be used if not set + # following attribute are used by default generation method + # they can be set to dict of strings using python formatting syntax + # dict keys will be used to select part to replace (e.g. "signals" key will + # replace ##SIGNALS_PART## in template), while the value is the format + # keys starting with "signal" will be used for signals, while ones starting with + # "method" will be used for methods + #  check D-Bus constructor for an example + CORE_FORMATS = None + CORE_TEMPLATE = None + CORE_DEST = None + FRONTEND_FORMATS = None + FRONTEND_TEMPLATE = None + FRONTEND_DEST = None + + # set to False if your bridge needs only core + FRONTEND_ACTIVATE = True + + def __init__(self, bridge_template, options): + self.bridge_template = bridge_template + self.args = options + + @property + def constructor_dir(self): + constructor_mod = import_module(self.__module__) + return os.path.dirname(constructor_mod.__file__) + + def getValues(self, name): + """Return values of a function in a dict + @param name: Name of the function to get + @return: dict, each key has the config value or None if the value is not set""" + function = {} + for option in ["type", "category", "sig_in", "sig_out", "doc"]: + try: + value = self.bridge_template.get(name, option) + except NoOptionError: + value = None + function[option] = value + return function + + def get_default(self, name): + """Return default values of a function in a dict + @param name: Name of the function to get + @return: dict, each key is the integer param number (no key if no default value)""" + default_dict = {} + def_re = re.compile(r"param_(\d+)_default") + + for option in self.bridge_template.options(name): + match = def_re.match(option) + if match: + try: + idx = int(match.group(1)) + except ValueError: + raise ParseError( + "Invalid value [%s] for parameter number" % match.group(1) + ) + default_dict[idx] = self.bridge_template.get(name, option) + + return default_dict + + def getFlags(self, name): + """Return list of flags set for this function + + @param name: Name of the function to get + @return: List of flags (string) + """ + flags = [] + for option in self.bridge_template.options(name): + if option in C.DECLARATION_FLAGS: + flags.append(option) + return flags + + def get_arguments_doc(self, name): + """Return documentation of arguments + @param name: Name of the function to get + @return: dict, each key is the integer param number (no key if no argument doc), value is a tuple (name, doc)""" + doc_dict = {} + option_re = re.compile(r"doc_param_(\d+)") + value_re = re.compile(r"^(\w+): (.*)$", re.MULTILINE | re.DOTALL) + for option in self.bridge_template.options(name): + if option == "doc_return": + doc_dict["return"] = self.bridge_template.get(name, option) + continue + match = option_re.match(option) + if match: + try: + idx = int(match.group(1)) + except ValueError: + raise ParseError( + "Invalid value [%s] for parameter number" % match.group(1) + ) + value_match = value_re.match(self.bridge_template.get(name, option)) + if not value_match: + raise ParseError("Invalid value for parameter doc [%i]" % idx) + doc_dict[idx] = (value_match.group(1), value_match.group(2)) + return doc_dict + + def get_doc(self, name): + """Return documentation of the method + @param name: Name of the function to get + @return: string documentation, or None""" + if self.bridge_template.has_option(name, "doc"): + return self.bridge_template.get(name, "doc") + return None + + def arguments_parser(self, signature): + """Generator which return individual arguments signatures from a global signature""" + start = 0 + i = 0 + + while i < len(signature): + if signature[i] not in ["b", "y", "n", "i", "x", "q", "u", "t", "d", "s", + "a"]: + raise ParseError("Unmanaged attribute type [%c]" % signature[i]) + + if signature[i] == "a": + i += 1 + if ( + signature[i] != "{" and signature[i] != "(" + ): # FIXME: must manage tuples out of arrays + i += 1 + yield signature[start:i] + start = i + continue # we have a simple type for the array + opening_car = signature[i] + assert opening_car in ["{", "("] + closing_car = "}" if opening_car == "{" else ")" + opening_count = 1 + while True: # we have a dict or a list of tuples + i += 1 + if i >= len(signature): + raise ParseError("missing }") + if signature[i] == opening_car: + opening_count += 1 + if signature[i] == closing_car: + opening_count -= 1 + if opening_count == 0: + break + i += 1 + yield signature[start:i] + start = i + + def get_arguments(self, signature, name=None, default=None, unicode_protect=False): + """Return arguments to user given a signature + + @param signature: signature in the short form (using s,a,i,b etc) + @param name: dictionary of arguments name like given by get_arguments_doc + @param default: dictionary of default values, like given by get_default + @param unicode_protect: activate unicode protection on strings (return strings as unicode(str)) + @return (str): arguments that correspond to a signature (e.g.: "sss" return "arg1, arg2, arg3") + """ + idx = 0 + attr_string = [] + + for arg in self.arguments_parser(signature): + attr_string.append( + ( + "str(%(name)s)%(default)s" + if (unicode_protect and arg == "s") + else "%(name)s%(default)s" + ) + % { + "name": name[idx][0] if (name and idx in name) else "arg_%i" % idx, + "default": "=" + default[idx] if (default and idx in default) else "", + } + ) + # give arg_1, arg2, etc or name1, name2=default, etc. + # give unicode(arg_1), unicode(arg_2), etc. if unicode_protect is set and arg is a string + idx += 1 + + return ", ".join(attr_string) + + def get_template_path(self, template_file): + """return template path corresponding to file name + + @param template_file(str): name of template file + """ + return os.path.join(self.constructor_dir, template_file) + + def core_completion_method(self, completion, function, default, arg_doc, async_): + """override this method to extend completion""" + pass + + def core_completion_signal(self, completion, function, default, arg_doc, async_): + """override this method to extend completion""" + pass + + def frontend_completion_method(self, completion, function, default, arg_doc, async_): + """override this method to extend completion""" + pass + + def frontend_completion_signal(self, completion, function, default, arg_doc, async_): + """override this method to extend completion""" + pass + + def generate(self, side): + """generate bridge + + call generate_core_side or generateFrontendSide if they exists + else call generic self._generate method + """ + try: + if side == "core": + method = self.generate_core_side + elif side == "frontend": + if not self.FRONTEND_ACTIVATE: + print("This constructor only handle core, please use core side") + sys.exit(1) + method = self.generateFrontendSide + except AttributeError: + self._generate(side) + else: + method() + + def _generate(self, side): + """generate the backend + + this is a generic method which will use formats found in self.CORE_SIGNAL_FORMAT + and self.CORE_METHOD_FORMAT (standard format method will be used) + @param side(str): core or frontend + """ + side_vars = [] + for var in ("FORMATS", "TEMPLATE", "DEST"): + attr = "{}_{}".format(side.upper(), var) + value = getattr(self, attr) + if value is None: + raise NotImplementedError + side_vars.append(value) + + FORMATS, TEMPLATE, DEST = side_vars + del side_vars + + parts = {part.upper(): [] for part in FORMATS} + sections = self.bridge_template.sections() + sections.sort() + for section in sections: + function = self.getValues(section) + print(("Adding %s %s" % (section, function["type"]))) + default = self.get_default(section) + arg_doc = self.get_arguments_doc(section) + async_ = "async" in self.getFlags(section) + completion = { + "sig_in": function["sig_in"] or "", + "sig_out": function["sig_out"] or "", + "category": "plugin" if function["category"] == "plugin" else "core", + "name": section, + # arguments with default values + "args": self.get_arguments( + function["sig_in"], name=arg_doc, default=default + ), + "args_no_default": self.get_arguments(function["sig_in"], name=arg_doc), + } + + extend_method = getattr( + self, "{}_completion_{}".format(side, function["type"]) + ) + extend_method(completion, function, default, arg_doc, async_) + + for part, fmt in FORMATS.items(): + if (part.startswith(function["type"]) + or part.startswith(f"async_{function['type']}")): + parts[part.upper()].append(fmt.format(**completion)) + + # at this point, signals_part, methods_part and direct_calls should be filled, + # we just have to place them in the right part of the template + bridge = [] + const_override = { + env[len(C.ENV_OVERRIDE) :]: v + for env, v in os.environ.items() + if env.startswith(C.ENV_OVERRIDE) + } + template_path = self.get_template_path(TEMPLATE) + try: + with open(template_path) as template: + for line in template: + + for part, extend_list in parts.items(): + if line.startswith("##{}_PART##".format(part)): + bridge.extend(extend_list) + break + else: + # the line is not a magic part replacement + if line.startswith("const_"): + const_name = line[len("const_") : line.find(" = ")].strip() + if const_name in const_override: + print(("const {} overriden".format(const_name))) + bridge.append( + "const_{} = {}".format( + const_name, const_override[const_name] + ) + ) + continue + bridge.append(line.replace("\n", "")) + except IOError: + print(("can't open template file [{}]".format(template_path))) + sys.exit(1) + + # now we write to final file + self.final_write(DEST, bridge) + + def final_write(self, filename, file_buf): + """Write the final generated file in [dest dir]/filename + + @param filename: name of the file to generate + @param file_buf: list of lines (stings) of the file + """ + if os.path.exists(self.args.dest_dir) and not os.path.isdir(self.args.dest_dir): + print( + "The destination dir [%s] can't be created: a file with this name already exists !" + ) + sys.exit(1) + try: + if not os.path.exists(self.args.dest_dir): + os.mkdir(self.args.dest_dir) + full_path = os.path.join(self.args.dest_dir, filename) + if os.path.exists(full_path) and not self.args.force: + print(( + "The destination file [%s] already exists ! Use --force to overwrite it" + % full_path + )) + try: + with open(full_path, "w") as dest_file: + dest_file.write("\n".join(file_buf)) + except IOError: + print(("Can't open destination file [%s]" % full_path)) + except OSError: + print("It's not possible to generate the file, check your permissions") + exit(1) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/bridge_constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/bridge_constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from libervia.backend.bridge import bridge_constructor +from libervia.backend.bridge.bridge_constructor.constants import Const as C +from libervia.backend.bridge.bridge_constructor import constructors, base_constructor +import argparse +from configparser import ConfigParser as Parser +from importlib import import_module +import os +import os.path + +# consts +__version__ = C.APP_VERSION + + +class BridgeConstructor(object): + def import_constructors(self): + constructors_dir = os.path.dirname(constructors.__file__) + self.protocoles = {} + for dir_ in os.listdir(constructors_dir): + init_path = os.path.join(constructors_dir, dir_, "__init__.py") + constructor_path = os.path.join(constructors_dir, dir_, "constructor.py") + module_path = "libervia.backend.bridge.bridge_constructor.constructors.{}.constructor".format( + dir_ + ) + if os.path.isfile(init_path) and os.path.isfile(constructor_path): + mod = import_module(module_path) + for attr in dir(mod): + obj = getattr(mod, attr) + if not isinstance(obj, type): + continue + if issubclass(obj, base_constructor.Constructor): + name = obj.NAME or dir_ + self.protocoles[name] = obj + break + if not self.protocoles: + raise ValueError("no protocole constructor found") + + def parse_args(self): + """Check command line options""" + parser = argparse.ArgumentParser( + description=C.DESCRIPTION, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument("--version", action="version", version=__version__) + default_protocole = ( + C.DEFAULT_PROTOCOLE + if C.DEFAULT_PROTOCOLE in self.protocoles + else self.protocoles[0] + ) + parser.add_argument( + "-p", + "--protocole", + choices=sorted(self.protocoles), + default=default_protocole, + help="generate bridge using PROTOCOLE (default: %(default)s)", + ) # (default: %s, possible values: [%s])" % (DEFAULT_PROTOCOLE, ", ".join(MANAGED_PROTOCOLES))) + parser.add_argument( + "-s", + "--side", + choices=("core", "frontend"), + default="core", + help="which side of the bridge do you want to make ?", + ) # (default: %default, possible values: [core, frontend])") + default_template = os.path.join( + os.path.dirname(bridge_constructor.__file__), "bridge_template.ini" + ) + parser.add_argument( + "-t", + "--template", + type=argparse.FileType(), + default=default_template, + help="use TEMPLATE to generate bridge (default: %(default)s)", + ) + parser.add_argument( + "-f", + "--force", + action="store_true", + help=("force overwritting of existing files"), + ) + parser.add_argument( + "-d", "--debug", action="store_true", help=("add debug information printing") + ) + parser.add_argument( + "--no-unicode", + action="store_false", + dest="unicode", + help=("remove unicode type protection from string results"), + ) + parser.add_argument( + "--flags", nargs="+", default=[], help=("constructors' specific flags") + ) + parser.add_argument( + "--dest-dir", + default=C.DEST_DIR_DEFAULT, + help=( + "directory when the generated files will be written (default: %(default)s)" + ), + ) + + return parser.parse_args() + + def go(self): + self.import_constructors() + args = self.parse_args() + template_parser = Parser() + try: + template_parser.read_file(args.template) + except IOError: + print("The template file doesn't exist or is not accessible") + exit(1) + constructor = self.protocoles[args.protocole](template_parser, args) + constructor.generate(args.side) + + +if __name__ == "__main__": + bc = BridgeConstructor() + bc.go() diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/bridge_template.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/bridge_template.ini Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1026 @@ +[DEFAULT] +doc_profile=profile: Name of the profile. +doc_profile_key=profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile. +doc_security_limit=security_limit: -1 means no security, 0 is the maximum security then the higher the less secure + +;signals + +[connected] +type=signal +category=core +sig_in=ss +doc=Connection is done +doc_param_0=jid_s: the JID that we were assigned by the server, as the resource might differ from the JID we asked for. +doc_param_1=%(doc_profile)s + +[disconnected] +type=signal +category=core +sig_in=s +doc=Connection is finished or lost +doc_param_0=%(doc_profile)s + +[contact_new] +type=signal +category=core +sig_in=sa{ss}ass +doc=New contact received in roster +doc_param_0=contact_jid: JID which has just been added +doc_param_1=attributes: Dictionary of attributes where keys are: + - name: name of the contact + - to: "True" if the contact give its presence information to us + - from: "True" if contact is registred to our presence information + - ask: "True" is subscription is pending +doc_param_2=groups: Roster's groups where the contact is +doc_param_3=%(doc_profile)s + +[message_new] +type=signal +category=core +sig_in=sdssa{ss}a{ss}sss +doc=A message has been received +doc_param_0=uid: unique ID of the message (id specific to SàT, this it *NOT* an XMPP id) +doc_param_1=timestamp: when the message was sent (or declared sent for delayed messages) +doc_param_2=from_jid: JID where the message is comming from +doc_param_3=to_jid: JID where the message must be sent +doc_param_4=message: message itself, can be in several languages (key is language code or '' for default) +doc_param_5=subject: subject of the message, can be in several languages (key is language code or '' for default) +doc_param_6=mess_type: Type of the message (cf RFC 6121 §5.2.2) + C.MESS_TYPE_INFO (system info) +doc_param_7=extra: extra message information, can have data added by plugins and/or: + - thread: id of the thread + - thread_parent: id of the parent of the current thread + - received_timestamp: date of receiption for delayed messages + - delay_sender: entity which has originally sent or which has delayed the message + - info_type: subtype for info messages +doc_param_8=%(doc_profile)s + +[message_encryption_started] +type=signal +category=core +sig_in=sss +doc=A message encryption session has been started +doc_param_0=to_jid: JID of the recipient (bare jid if it's encrypted for all devices) +doc_param_1=encryption_data: (JSON_OBJ) data of the encryption algorithm used, encoded as a json object. + it always has the following keys: + - name: human readable name of the algorithm + - namespace: namespace of the encryption plugin + following keys are present if suitable: + - directed_devices: list or resource where session is encrypted +doc_param_2=%(doc_profile_key)s + +[message_encryption_stopped] +type=signal +category=core +sig_in=sa{ss}s +doc=A message encryption session has been stopped +doc_param_0=to_jid: JID of the recipient (full jid if it's only stopped for one device) +doc_param_1=encryption_data: data of the encryption algorithm stopped, has a least following keys: + - name: human readable name of the algorithm + - namespace: namespace of the encryption plugin +doc_param_2=%(doc_profile_key)s + +[presence_update] +type=signal +category=core +sig_in=ssia{ss}s +doc=Somebody changed his presence information. +doc_param_0=entity_jid: JID from which we have presence informatios +doc_param_1=show: availability status (see RFC 6121 §4.7.2.1) +doc_param_2=priority: Priority level of the ressource (see RFC 6121 §4.7.2.3) +doc_param_3=statuses: Natural language description of the availability status (see RFC 6121 §4.7.2.2) +doc_param_4=%(doc_profile)s + +[subscribe] +type=signal +category=core +sig_in=sss +doc=Somebody wants to be added in roster +doc_param_0=sub_type: Subscription states (see RFC 6121 §3) +doc_param_1=entity_jid: JID from which the subscription is coming +doc_param_2=%(doc_profile)s + +[param_update] +type=signal +category=core +sig_in=ssss +doc=A parameter has been changed +doc_param_0=name: Name of the updated parameter +doc_param_1=value: New value of the parameter +doc_param_2=category: Category of the updated parameter +doc_param_3=%(doc_profile)s + +[contact_deleted] +type=signal +category=core +sig_in=ss +doc=A contact has been supressed from roster +doc_param_0=entity_jid: JID of the contact removed from roster +doc_param_1=%(doc_profile)s + +[action_new] +type=signal +category=core +sig_in=ssis +doc=A frontend action is requested +doc_param_0=action_data: a serialised dict where key can be: + - xmlui: a XMLUI describing the action + - progress: a progress id + - meta_*: meta information on the action, used to make automation more easy, + some are defined below + - meta_from_jid: origin of the request + - meta_type: type of the request, can be one of: + - C.META_TYPE_FILE: a file transfer request validation + - C.META_TYPE_OVERWRITE: a file overwriting confirmation + - meta_progress_id: progress id linked to this action +doc_param_1=id: action id + This id can be used later by frontends to announce to other ones that the action is managed and can now be ignored. +doc_param_2=%(doc_security_limit)s +doc_param_3=%(doc_profile)s + +[entity_data_updated] +type=signal +category=core +sig_in=ssss +doc=An entity's data has been updated +doc_param_0=jid: entity's bare jid +doc_param_1=name: Name of the updated value +doc_param_2=value: New value +doc_param_3=%(doc_profile)s + +[progress_started] +type=signal +category=core +sig_in=sa{ss}s +doc=A progressing operation has just started +doc_param_0=id: id of the progression operation +doc_param_1=metadata: dict of progress metadata, key can be: + - name: name of the progression, full path for a file + - direction: "in" for incoming data, "out" else + - type: type of the progression: + C.META_TYPE_FILE: file transfer +doc_param_2=%(doc_profile)s + +[progress_finished] +type=signal +category=core +sig_in=sa{ss}s +doc=A progressing operation is finished +doc_param_0=id: id of the progression operation +doc_param_1=metadata: dict of progress status metadata, key can be: + - hash: value of the computed hash + - hash_algo: alrorithm used to compute hash + - hash_verified: C.BOOL_TRUE if hash is verified and OK + C.BOOL_FALSE if hash was not received ([progress_error] will be used if there is a mismatch) + - url: url linked to the progression (e.g. download url after a file upload) +doc_param_2=%(doc_profile)s + +[progress_error] +type=signal +category=core +sig_in=sss +doc=There was an error during progressing operation +doc_param_0=id: id of the progression operation +doc_param_1=error: error message +doc_param_2=%(doc_profile)s + +[_debug] +type=signal +category=core +sig_in=sa{ss}s +doc=Debug method, useful for developers +doc_param_0=action: action to do +doc_param_1=params: action parameters +doc_param_2=%(doc_profile)s + +;methods + +[ready_get] +async= +type=method +category=core +sig_in= +sig_out= +doc=Return when backend is initialised + +[version_get] +type=method +category=core +sig_in= +sig_out=s +doc=Get "Salut à Toi" full version + +[features_get] +type=method +category=core +sig_in=s +sig_out=a{sa{ss}} +doc=Get available features and plugins + features can changes for differents profiles, e.g. because of differents server capabilities +doc_param_0=%(doc_profile_key)s +doc_return=dictionary of available features: + plugin import name is used as key, data is an other dict managed by the plugin +async= + +[profile_name_get] +type=method +category=core +sig_in=s +sig_out=s +param_0_default="@DEFAULT@" +doc=Get real profile name from profile key +doc_param_0=%(doc_profile_key)s +doc_return=Real profile name + +[profiles_list_get] +type=method +category=core +sig_in=bb +sig_out=as +param_0_default=True +param_1_default=False +doc_param_0=clients: get clients profiles +doc_param_1=components: get components profiles +doc=Get list of profiles + +[profile_set_default] +type=method +category=core +sig_in=s +sig_out= +doc_param_0=%(doc_profile)s +doc=Set default profile + +[entity_data_get] +type=method +category=core +sig_in=sass +sig_out=a{ss} +doc=Get data in cache for an entity +doc_param_0=jid: entity's bare jid +doc_param_1=keys: list of keys to get +doc_param_2=%(doc_profile)s +doc_return=dictionary of asked key, + if key doesn't exist, the resulting dictionary will not have the key + +[entities_data_get] +type=method +category=core +sig_in=asass +sig_out=a{sa{ss}} +doc=Get data in cache for several entities at once +doc_param_0=jids: list of entities bare jid, or empty list to have all jids in cache +doc_param_1=keys: list of keys to get +doc_param_2=%(doc_profile)s +doc_return=dictionary with jids as keys and dictionary of asked key as values + values are serialised + if key doesn't exist for a jid, the resulting dictionary will not have it + +[profile_create] +async= +type=method +category=core +sig_in=sss +sig_out= +param_1_default='' +param_2_default='' +doc=Create a new profile +doc_param_0=%(doc_profile)s +doc_param_1=password: password of the profile +doc_param_2=component: set to component entry point if it is a component, else use empty string +doc_return=callback is called when profile actually exists in database and memory +errback is called with error constant as parameter: + - ConflictError: the profile name already exists + - CancelError: profile creation canceled + - NotFound: component entry point is not available + +[profile_delete_async] +async= +type=method +category=core +sig_in=s +sig_out= +doc=Delete a profile +doc_param_0=%(doc_profile)s +doc_return=callback is called when profile has been deleted from database and memory +errback is called with error constant as parameter: + - ProfileUnknownError: the profile name is unknown + - ConnectedProfileError: a connected profile would not be deleted + +[connect] +async= +type=method +category=core +sig_in=ssa{ss} +sig_out=b +param_0_default="@DEFAULT@" +param_1_default='' +param_2_default={} +doc=Connect a profile +doc_param_0=%(doc_profile_key)s +doc_param_1=password: the SàT profile password +doc_param_2=options: connection options +doc_return=a deferred boolean or failure: + - boolean if the profile authentication succeed: + - True if the XMPP connection was already established + - False if the XMPP connection has been initiated (it may still fail) + - failure if the profile authentication failed + +[profile_start_session] +async= +type=method +category=core +sig_in=ss +sig_out=b +param_0_default='' +param_1_default="@DEFAULT@" +doc=Start a profile session without connecting it (if it's not already the case) +doc_param_0=password: the SàT profile password +doc_param_1=%(doc_profile_key)s +doc_return=D(bool): + - True if the profile session was already started + - False else + +[profile_is_session_started] +type=method +category=core +sig_in=s +sig_out=b +param_0_default="@DEFAULT@" +doc=Tell if a profile session is loaded +doc_param_0=%(doc_profile_key)s + +[disconnect] +async= +type=method +category=core +sig_in=s +sig_out= +param_0_default="@DEFAULT@" +doc=Disconnect a profile +doc_param_0=%(doc_profile_key)s + +[is_connected] +type=method +category=core +sig_in=s +sig_out=b +param_0_default="@DEFAULT@" +doc=Tell if a profile is connected +doc_param_0=%(doc_profile_key)s + +[contact_get] +async= +type=method +category=core +sig_in=ss +sig_out=(a{ss}as) +param_1_default="@DEFAULT@" +doc=Return informations in roster about a contact +doc_param_1=%(doc_profile_key)s +doc_return=tuple with the following values: + - list of attributes as in [contact_new] + - groups where the contact is + +[contacts_get] +async= +type=method +category=core +sig_in=s +sig_out=a(sa{ss}as) +param_0_default="@DEFAULT@" +doc=Return information about all contacts (the roster) +doc_param_0=%(doc_profile_key)s +doc_return=array of tuples with the following values: + - JID of the contact + - list of attributes as in [contact_new] + - groups where the contact is + +[contacts_get_from_group] +type=method +category=core +sig_in=ss +sig_out=as +param_1_default="@DEFAULT@" +doc=Return information about all contacts +doc_param_0=group: name of the group to check +doc_param_1=%(doc_profile_key)s +doc_return=array of jids + +[main_resource_get] +type=method +category=core +sig_in=ss +sig_out=s +param_1_default="@DEFAULT@" +doc=Return the last resource connected for a contact +doc_param_0=contact_jid: jid of the contact +doc_param_1=%(doc_profile_key)s +doc_return=the resource connected of the contact with highest priority, or "" + +[presence_statuses_get] +type=method +category=core +sig_in=s +sig_out=a{sa{s(sia{ss})}} +param_0_default="@DEFAULT@" +doc=Return presence information of all contacts +doc_param_0=%(doc_profile_key)s +doc_return=Dict of presence with bare JID of contact as key, and value as follow: + A dict where key is the resource and the value is a tuple with (show, priority, statuses) as for [presence_update] + +[sub_waiting_get] +type=method +category=core +sig_in=s +sig_out=a{ss} +param_0_default="@DEFAULT@" +doc=Get subscription requests in queue +doc_param_0=%(doc_profile_key)s +doc_return=Dict where contact JID is the key, and value is the subscription type + +[message_send] +async= +type=method +category=core +sig_in=sa{ss}a{ss}sss +sig_out= +param_2_default={} +param_3_default="auto" +param_4_default={} +param_5_default="@NONE@" +doc=Send a message +doc_param_0=to_jid: JID of the recipient +doc_param_1=message: body of the message: + key is the language of the body, use '' when unknown +doc_param_2=subject: Subject of the message + key is the language of the subject, use '' when unknown +doc_param_3=mess_type: Type of the message (cf RFC 6121 §5.2.2) or "auto" for automatic type detection +doc_param_4=extra: (serialised) optional data that can be used by a plugin to build more specific messages +doc_param_5=%(doc_profile_key)s + +[message_encryption_start] +async= +type=method +category=core +sig_in=ssbs +sig_out= +param_1_default='' +param_2_default=False +param_3_default="@NONE@" +doc=Start an encryption session +doc_param_0=to_jid: JID of the recipient (bare jid if it must be encrypted for all devices) +doc_param_1=namespace: namespace of the encryption algorithm to use +doc_param_2=replace: if True and an encryption session already exists, it will be replaced by this one + else a ConflictError will be raised +doc_param_3=%(doc_profile_key)s + +[message_encryption_stop] +async= +type=method +category=core +sig_in=ss +sig_out= +doc=Stop an encryption session +doc_param_0=to_jid: JID of the recipient (full jid if encryption must be stopped for one device only) +doc_param_1=%(doc_profile_key)s + +[message_encryption_get] +type=method +category=core +sig_in=ss +sig_out=s +doc=Retrieve encryption data for a given entity +doc_param_0=to_jid: bare JID of the recipient +doc_param_1=%(doc_profile_key)s +doc_return=(JSON_OBJ) empty string if session is unencrypted, else a json encoded objects. + In case of dict, following keys are always present: + - name: human readable name of the encryption algorithm + - namespace: namespace of the plugin + following key can be present if suitable: + - directed_devices: list or resource where session is encrypted + +[encryption_namespace_get] +type=method +category=core +sig_in=s +sig_out=s +doc=Get algorithm namespace from its name + +[encryption_plugins_get] +type=method +category=core +sig_in= +sig_out=s +doc=Retrieve registered plugins for encryption + +[encryption_trust_ui_get] +async= +type=method +category=core +sig_in=sss +sig_out=s +doc=Get XMLUI to manage trust for given encryption algorithm +doc_param_0=to_jid: bare JID of entity to manage +doc_param_1=namespace: namespace of the algorithm to manage +doc_param_2=%(doc_profile_key)s +doc_return=(XMLUI) UI of the trust management + +[presence_set] +type=method +category=core +sig_in=ssa{ss}s +sig_out= +param_0_default='' +param_1_default='' +param_2_default={} +param_3_default="@DEFAULT@" +doc=Set presence information for the profile +doc_param_0=to_jid: the JID to who we send the presence data (emtpy string for broadcast) +doc_param_1=show: as for [presence_update] +doc_param_2=statuses: as for [presence_update] +doc_param_3=%(doc_profile_key)s + +[subscription] +type=method +category=core +sig_in=sss +sig_out= +param_2_default="@DEFAULT@" +doc=Send subscription request/answer to a contact +doc_param_0=sub_type: as for [subscribe] +doc_param_1=entity: as for [subscribe] +doc_param_2=%(doc_profile_key)s + +[config_get] +type=method +category=core +sig_in=ss +sig_out=s +doc=get main configuration option +doc_param_0=section: section of the configuration file (empty string for DEFAULT) +doc_param_1=name: name of the option + +[param_set] +type=method +category=core +sig_in=sssis +sig_out= +param_3_default=-1 +param_4_default="@DEFAULT@" +doc=Change a parameter +doc_param_0=name: Name of the parameter to change +doc_param_1=value: New Value of the parameter +doc_param_2=category: Category of the parameter to change +doc_param_3=%(doc_security_limit)s +doc_param_4=%(doc_profile_key)s + +[param_get_a] +type=method +category=core +sig_in=ssss +sig_out=s +param_2_default="value" +param_3_default="@DEFAULT@" +doc=Helper method to get a parameter's attribute *when profile is connected* +doc_param_0=name: as for [param_set] +doc_param_1=category: as for [param_set] +doc_param_2=attribute: Name of the attribute +doc_param_3=%(doc_profile_key)s + +[private_data_get] +async= +type=method +category=core +sig_in=sss +sig_out=s +doc=Retrieve private data +doc_param_0=namespace: unique namespace to use +doc_param_1=key: key of the data to set +doc_param_2=%(doc_profile_key)s +doc_return=serialised data + +[private_data_set] +async= +type=method +category=core +sig_in=ssss +sig_out= +doc=Store private data +doc_param_0=namespace: unique namespace to use +doc_param_1=key: key of the data to set +doc_param_2=data: serialised data +doc_param_3=%(doc_profile_key)s + +[private_data_delete] +async= +type=method +category=core +sig_in=sss +sig_out= +doc=Delete private data +doc_param_0=namespace: unique namespace to use +doc_param_1=key: key of the data to delete +doc_param_3=%(doc_profile_key)s + +[param_get_a_async] +async= +type=method +category=core +sig_in=sssis +sig_out=s +param_2_default="value" +param_3_default=-1 +param_4_default="@DEFAULT@" +doc=Helper method to get a parameter's attribute +doc_param_0=name: as for [param_set] +doc_param_1=category: as for [param_set] +doc_param_2=attribute: Name of the attribute +doc_param_3=%(doc_security_limit)s +doc_param_4=%(doc_profile_key)s + +[params_values_from_category_get_async] +async= +type=method +category=code +sig_in=sisss +sig_out=a{ss} +param_1_default=-1 +param_2_default="" +param_3_default="" +param_4_default="@DEFAULT@" +doc=Get "attribute" for all params of a category +doc_param_0=category: as for [param_set] +doc_param_1=%(doc_security_limit)s +doc_param_2=app: name of the frontend requesting the parameters, or '' to get all parameters +doc_param_3=extra: extra options/filters +doc_param_4=%(doc_profile_key)s + +[param_ui_get] +async= +type=method +category=core +sig_in=isss +sig_out=s +param_0_default=-1 +param_1_default='' +param_2_default='' +param_3_default="@DEFAULT@" +doc=Return a SàT XMLUI for parameters, eventually restrict the result to the parameters concerning a given frontend +doc_param_0=%(doc_security_limit)s +doc_param_1=app: name of the frontend requesting the parameters, or '' to get all parameters +doc_param_2=extra: extra options/filters +doc_param_3=%(doc_profile_key)s + +[params_categories_get] +type=method +category=core +sig_in= +sig_out=as +doc=Get all categories currently existing in parameters +doc_return=list of categories + +[params_register_app] +type=method +category=core +sig_in=sis +sig_out= +param_1_default=-1 +param_2_default='' +doc=Register frontend's specific parameters +doc_param_0=xml: XML definition of the parameters to be added +doc_param_1=%(doc_security_limit)s +doc_param_2=app: name of the frontend registering the parameters + +[history_get] +async= +type=method +category=core +sig_in=ssiba{ss}s +sig_out=a(sdssa{ss}a{ss}ss) +param_3_default=True +param_4_default='' +param_5_default="@NONE@" +doc=Get history of a communication between two entities +doc_param_0=from_jid: source JID (bare jid for catch all, full jid else) +doc_param_1=to_jid: dest JID (bare jid for catch all, full jid else) +doc_param_2=limit: max number of history elements to get (0 for the whole history) +doc_param_3=between: True if we want history between the two jids (in both direction), False if we only want messages from from_jid to to_jid +doc_param_4=filters: patterns to filter the history results, can be: + - body: pattern must be in message body + - search: pattern must be in message body or source resource + - types: type must be one of those, values are separated by spaces + - not_types: type must not be one of those, values are separated by spaces + - before_uid: check only message received before message with given uid +doc_param_5=%(doc_profile)s +doc_return=Ordered list (by timestamp) of data as in [message_new] (without final profile) + +[contact_add] +type=method +category=core +sig_in=ss +sig_out= +param_1_default="@DEFAULT@" +doc=Add a contact to profile's roster +doc_param_0=entity_jid: JID to add to roster +doc_param_1=%(doc_profile_key)s + +[contact_update] +type=method +category=core +sig_in=ssass +sig_out= +param_3_default="@DEFAULT@" +doc=update a contact in profile's roster +doc_param_0=entity_jid: JID update in roster +doc_param_1=name: roster's name for the entity +doc_param_2=groups: list of group where the entity is +doc_param_3=%(doc_profile_key)s + +[contact_del] +async= +type=method +category=core +sig_in=ss +sig_out= +param_1_default="@DEFAULT@" +doc=Remove a contact from profile's roster +doc_param_0=entity_jid: JID to remove from roster +doc_param_1=%(doc_profile_key)s + +[roster_resync] +async= +type=method +category=core +sig_in=s +sig_out= +param_0_default="@DEFAULT@" +doc=Do a full resynchronisation of roster with server +doc_param_0=%(doc_profile_key)s + +[action_launch] +async= +type=method +category=core +sig_in=sss +sig_out=s +param_2_default="@DEFAULT@" +doc=Launch a registred action +doc_param_0=callback_id: id of the registred callback +doc_param_1=data: optional data +doc_param_2=%(doc_profile_key)s +doc_return=dict where key can be: + - xmlui: a XMLUI need to be displayed + +[actions_get] +type=method +category=core +sig_in=s +sig_out=a(ssi) +param_0_default="@DEFAULT@" +doc=Get all not yet answered actions +doc_param_0=%(doc_profile_key)s +doc_return=list of data as for [action_new] (without the profile) + +[progress_get] +type=method +category=core +sig_in=ss +sig_out=a{ss} +doc=Get progress information for an action +doc_param_0=id: id of the progression status +doc_param_1=%(doc_profile)s +doc_return=dict with progress informations: + - position: current position + - size: end position (optional if not known) + other metadata may be present + +[progress_get_all_metadata] +type=method +category=core +sig_in=s +sig_out=a{sa{sa{ss}}} +doc=Get all active progress informations +doc_param_0=%(doc_profile)s or C.PROF_KEY_ALL for all profiles +doc_return= a dict which map profile to progress_dict + progress_dict map progress_id to progress_metadata + progress_metadata is the same dict as sent by [progress_started] + +[progress_get_all] +type=method +category=core +sig_in=s +sig_out=a{sa{sa{ss}}} +doc=Get all active progress informations +doc_param_0=%(doc_profile)s or C.PROF_KEY_ALL for all profiles +doc_return= a dict which map profile to progress_dict + progress_dict map progress_id to progress_data + progress_data is the same dict as returned by [progress_get] + +[menus_get] +type=method +category=core +sig_in=si +sig_out=a(ssasasa{ss}) +doc=Get all additional menus +doc_param_0=language: language in which the menu should be translated (empty string for default) +doc_param_1=security_limit: %(doc_security_limit)s +doc_return=list of tuple with the following value: + - menu_id: menu id (same as callback id) + - menu_type: Type which can be: + * NORMAL: Classical application menu + - menu_path: raw path of the menu + - menu_path_i18n: translated path of the menu + - extra: extra data, like icon name + +[menu_launch] +async= +type=method +category=core +sig_in=sasa{ss}is +sig_out=a{ss} +doc=Launch a registred menu +doc_param_0=menu_type: type of the menu (C.MENU_*) +doc_param_1=path: canonical (untranslated) path of the menu +doc_param_2=data: optional data +doc_param_3=%(doc_security_limit)s +doc_param_4=%(doc_profile_key)s +doc_return=dict where key can be: + - xmlui: a XMLUI need to be displayed + +[menu_help_get] +type=method +category=core +sig_in=ss +sig_out=s +param_2="NORMAL" +doc=Get help information for a menu +doc_param_0=menu_id: id of the menu (same as callback_id) +doc_param_1=language: language in which the menu should be translated (empty string for default) +doc_return=Translated help string + +[disco_infos] +async= +type=method +category=core +sig_in=ssbs +sig_out=(asa(sss)a{sa(a{ss}as)}) +param_1_default=u'' +param_2_default=True +param_3_default="@DEFAULT@" +doc=Discover infos on an entity +doc_param_0=entity_jid: JID to discover +doc_param_1=node: node to use +doc_param_2=use_cache: use cached data if available +doc_param_3=%(doc_profile_key)s +doc_return=discovery data: + - list of features + - list of identities (category, type, name) + - dictionary of extensions (FORM_TYPE as key), with value of: + - list of field which are: + - dictionary key/value where key can be: + * var + * label + * type + * desc + - list of values + +[disco_items] +async= +type=method +category=core +sig_in=ssbs +sig_out=a(sss) +param_1_default=u'' +param_2_default=True +param_3_default="@DEFAULT@" +doc=Discover items of an entity +doc_param_0=entity_jid: JID to discover +doc_param_1=node: node to use +doc_param_2=use_cache: use cached data if available +doc_param_3=%(doc_profile_key)s +doc_return=array of tuple (entity, node identifier, name) + +[disco_find_by_features] +async= +type=method +category=core +sig_in=asa(ss)bbbbbs +sig_out=(a{sa(sss)}a{sa(sss)}a{sa(sss)}) +param_2_default=False +param_3_default=True +param_4_default=True +param_5_default=True +param_6_default=False +param_7_default="@DEFAULT@" +doc=Discover items of an entity +doc_param_0=namespaces: namespaces of the features to check +doc_param_1=identities: identities to filter +doc_param_2=bare_jid: if True only retrieve bare jids + if False, retrieve full jids of connected resources +doc_param_3=service: True to check server's services +doc_param_4=roster: True to check connected devices from people in roster +doc_param_5=own_jid: True to check profile's jid +doc_param_6=local_device: True to check device on which the backend is running +doc_param_7=%(doc_profile_key)s +doc_return=tuple of maps of found entities full jids to their identities. Maps are in this order: + - services entities + - own entities (i.e. entities linked to profile's jid) + - roster entities + +[params_template_save] +type=method +category=core +sig_in=s +sig_out=b +doc=Save parameters template to xml file +doc_param_0=filename: output filename +doc_return=boolean (True in case of success) + +[params_template_load] +type=method +category=core +sig_in=s +sig_out=b +doc=Load parameters template from xml file +doc_param_0=filename: input filename +doc_return=boolean (True in case of success) + +[session_infos_get] +async= +type=method +category=core +sig_in=s +sig_out=a{ss} +doc=Get various informations on current profile session +doc_param_0=%(doc_profile_key)s +doc_return=session informations, with at least the following keys: + jid: current full jid + started: date of creation of the session (Epoch time) + +[devices_infos_get] +async= +type=method +category=core +sig_in=ss +sig_out=s +doc=Get various informations on an entity devices +doc_param_0=bare_jid: get data on known devices from this entity + empty string to get devices of the profile +doc_param_1=%(doc_profile_key)s +doc_return=list of known devices, where each item is a dict with a least following keys: + resource: device resource + +[namespaces_get] +type=method +category=core +sig_in= +sig_out=a{ss} +doc=Get a dict to short name => whole namespaces +doc_return=namespaces mapping + +[image_check] +type=method +category=core +sig_in=s +sig_out=s +doc=Analyze an image a return a report +doc_return=serialized report + +[image_resize] +async= +type=method +category=core +sig_in=sii +sig_out=s +doc=Create a new image with desired size +doc_param_0=image_path: path of the image to resize +doc_param_1=width: width of the new image +doc_param_2=height: height of the new image +doc_return=path of the new image with desired size + the image must be deleted once not needed anymore + +[image_generate_preview] +async= +type=method +category=core +sig_in=ss +sig_out=s +doc=Generate a preview of an image in cache +doc_param_0=image_path: path of the original image +doc_param_1=%(doc_profile_key)s +doc_return=path to the preview in cache + +[image_convert] +async= +type=method +category=core +sig_in=ssss +sig_out=s +doc=Convert an image to an other format +doc_param_0=source: path of the image to convert +doc_param_1=dest: path to the location where the new image must be stored. + Empty string to generate a file in cache, unique to the source +doc_param_3=extra: serialised extra +doc_param_4=profile_key: either profile_key or empty string to use common cache + this parameter is used only when dest is empty +doc_return=path to the new converted image diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constants.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core import constants + + +class Const(constants.Const): + + NAME = "bridge_constructor" + DEST_DIR_DEFAULT = "generated" + DESCRIPTION = """{name} Copyright (C) 2009-2021 Jérôme Poisson (aka Goffi) + + This script construct a SàT bridge using the given protocol + + This program comes with ABSOLUTELY NO WARRANTY; + This is free software, and you are welcome to redistribute it + under certain conditions. + """.format( + name=NAME, version=constants.Const.APP_VERSION + ) + #  TODO: move protocoles in separate files (plugins?) + DEFAULT_PROTOCOLE = "dbus" + + # flags used method/signal declaration (not to be confused with constructor flags) + DECLARATION_FLAGS = ["deprecated", "async"] + + ENV_OVERRIDE = "SAT_BRIDGE_CONST_" # Prefix used to override a constant diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus-xml/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus-xml/constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus-xml/constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.bridge_constructor import base_constructor +from xml.dom import minidom +import sys + + +class DbusXmlConstructor(base_constructor.Constructor): + """Constructor for DBus XML syntaxt (used by Qt frontend)""" + + def __init__(self, bridge_template, options): + base_constructor.Constructor.__init__(self, bridge_template, options) + + self.template = "dbus_xml_template.xml" + self.core_dest = "org.libervia.sat.xml" + self.default_annotation = { + "a{ss}": "StringDict", + "a(sa{ss}as)": "QList", + "a{i(ss)}": "HistoryT", + "a(sss)": "QList", + "a{sa{s(sia{ss})}}": "PresenceStatusT", + } + + def generate_core_side(self): + try: + doc = minidom.parse(self.get_template_path(self.template)) + interface_elt = doc.getElementsByTagName("interface")[0] + except IOError: + print("Can't access template") + sys.exit(1) + except IndexError: + print("Template error") + sys.exit(1) + + sections = self.bridge_template.sections() + sections.sort() + for section in sections: + function = self.getValues(section) + print(("Adding %s %s" % (section, function["type"]))) + new_elt = doc.createElement( + "method" if function["type"] == "method" else "signal" + ) + new_elt.setAttribute("name", section) + + idx = 0 + args_doc = self.get_arguments_doc(section) + for arg in self.arguments_parser(function["sig_in"] or ""): + arg_elt = doc.createElement("arg") + arg_elt.setAttribute( + "name", args_doc[idx][0] if idx in args_doc else "arg_%i" % idx + ) + arg_elt.setAttribute("type", arg) + _direction = "in" if function["type"] == "method" else "out" + arg_elt.setAttribute("direction", _direction) + new_elt.appendChild(arg_elt) + if "annotation" in self.args.flags: + if arg in self.default_annotation: + annot_elt = doc.createElement("annotation") + annot_elt.setAttribute( + "name", "com.trolltech.QtDBus.QtTypeName.In%d" % idx + ) + annot_elt.setAttribute("value", self.default_annotation[arg]) + new_elt.appendChild(annot_elt) + idx += 1 + + if function["sig_out"]: + arg_elt = doc.createElement("arg") + arg_elt.setAttribute("type", function["sig_out"]) + arg_elt.setAttribute("direction", "out") + new_elt.appendChild(arg_elt) + if "annotation" in self.args.flags: + if function["sig_out"] in self.default_annotation: + annot_elt = doc.createElement("annotation") + annot_elt.setAttribute( + "name", "com.trolltech.QtDBus.QtTypeName.Out0" + ) + annot_elt.setAttribute( + "value", self.default_annotation[function["sig_out"]] + ) + new_elt.appendChild(annot_elt) + + interface_elt.appendChild(new_elt) + + # now we write to final file + self.final_write(self.core_dest, [doc.toprettyxml()]) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus-xml/dbus_xml_template.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus-xml/dbus_xml_template.xml Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,4 @@ + + + + diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus/constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus/constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.bridge_constructor import base_constructor + + +class DbusConstructor(base_constructor.Constructor): + NAME = "dbus" + CORE_TEMPLATE = "dbus_core_template.py" + CORE_DEST = "dbus_bridge.py" + CORE_FORMATS = { + "methods_declarations": """\ + Method('{name}', arguments='{sig_in}', returns='{sig_out}'),""", + + "methods": """\ + def dbus_{name}(self, {args}): + {debug}return self._callback("{name}", {args_no_default})\n""", + + "signals_declarations": """\ + Signal('{name}', '{sig_in}'),""", + + "signals": """\ + def {name}(self, {args}): + self._obj.emitSignal("{name}", {args})\n""", + } + + FRONTEND_TEMPLATE = "dbus_frontend_template.py" + FRONTEND_DEST = CORE_DEST + FRONTEND_FORMATS = { + "methods": """\ + def {name}(self, {args}{async_comma}{async_args}): + {error_handler}{blocking_call}{debug}return {result}\n""", + "async_methods": """\ + def {name}(self{async_comma}{args}): + loop = asyncio.get_running_loop() + fut = loop.create_future() + reply_handler = lambda ret=None: loop.call_soon_threadsafe(fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe(fut.set_exception, dbus_to_bridge_exception(err)) + self.db_{category}_iface.{name}({args_result}{async_comma}timeout=const_TIMEOUT, reply_handler=reply_handler, error_handler=error_handler) + {debug}return fut\n""", + } + + def core_completion_signal(self, completion, function, default, arg_doc, async_): + completion["category"] = completion["category"].upper() + completion["body"] = ( + "pass" + if not self.args.debug + else 'log.debug ("{}")'.format(completion["name"]) + ) + + def core_completion_method(self, completion, function, default, arg_doc, async_): + completion.update( + { + "debug": ( + "" if not self.args.debug + else f'log.debug ("{completion["name"]}")\n{8 * " "}' + ) + } + ) + + def frontend_completion_method(self, completion, function, default, arg_doc, async_): + completion.update( + { + # XXX: we can manage blocking call in the same way as async one: if callback is None the call will be blocking + "debug": "" + if not self.args.debug + else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " "), + "args_result": self.get_arguments(function["sig_in"], name=arg_doc), + "async_args": "callback=None, errback=None", + "async_comma": ", " if function["sig_in"] else "", + "error_handler": """if callback is None: + error_handler = None + else: + if errback is None: + errback = log.error + error_handler = lambda err:errback(dbus_to_bridge_exception(err)) + """, + } + ) + if async_: + completion["blocking_call"] = "" + completion[ + "async_args_result" + ] = "timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler" + else: + # XXX: To have a blocking call, we must have not reply_handler, so we test if callback exists, and add reply_handler only in this case + completion[ + "blocking_call" + ] = """kwargs={} + if callback is not None: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = callback + kwargs['error_handler'] = error_handler + """ + completion["async_args_result"] = "**kwargs" + result = ( + "self.db_%(category)s_iface.%(name)s(%(args_result)s%(async_comma)s%(async_args_result)s)" + % completion + ) + completion["result"] = ( + "str(%s)" if self.args.unicode and function["sig_out"] == "s" else "%s" + ) % result diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 + +# Libervia communication bridge +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from types import MethodType +from functools import partialmethod +from twisted.internet import defer, reactor +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.core.exceptions import BridgeInitError +from libervia.backend.tools import config +from txdbus import client, objects, error +from txdbus.interface import DBusInterface, Method, Signal + + +log = getLogger(__name__) + +# Interface prefix +const_INT_PREFIX = config.config_get( + config.parse_main_conf(), + "", + "bridge_dbus_int_prefix", + "org.libervia.Libervia") +const_ERROR_PREFIX = const_INT_PREFIX + ".error" +const_OBJ_PATH = "/org/libervia/Libervia/bridge" +const_CORE_SUFFIX = ".core" +const_PLUGIN_SUFFIX = ".plugin" + + +class ParseError(Exception): + pass + + +class DBusException(Exception): + pass + + +class MethodNotRegistered(DBusException): + dbusErrorName = const_ERROR_PREFIX + ".MethodNotRegistered" + + +class GenericException(DBusException): + def __init__(self, twisted_error): + """ + + @param twisted_error (Failure): instance of twisted Failure + error message is used to store a repr of message and condition in a tuple, + so it can be evaluated by the frontend bridge. + """ + try: + # twisted_error.value is a class + class_ = twisted_error.value().__class__ + except TypeError: + # twisted_error.value is an instance + class_ = twisted_error.value.__class__ + data = twisted_error.getErrorMessage() + try: + data = (data, twisted_error.value.condition) + except AttributeError: + data = (data,) + else: + data = (str(twisted_error),) + self.dbusErrorName = ".".join( + (const_ERROR_PREFIX, class_.__module__, class_.__name__) + ) + super(GenericException, self).__init__(repr(data)) + + @classmethod + def create_and_raise(cls, exc): + raise cls(exc) + + +class DBusObject(objects.DBusObject): + + core_iface = DBusInterface( + const_INT_PREFIX + const_CORE_SUFFIX, +##METHODS_DECLARATIONS_PART## +##SIGNALS_DECLARATIONS_PART## + ) + plugin_iface = DBusInterface( + const_INT_PREFIX + const_PLUGIN_SUFFIX + ) + + dbusInterfaces = [core_iface, plugin_iface] + + def __init__(self, path): + super().__init__(path) + log.debug("Init DBusObject...") + self.cb = {} + + def register_method(self, name, cb): + self.cb[name] = cb + + def _callback(self, name, *args, **kwargs): + """Call the callback if it exists, raise an exception else""" + try: + cb = self.cb[name] + except KeyError: + raise MethodNotRegistered + else: + d = defer.maybeDeferred(cb, *args, **kwargs) + d.addErrback(GenericException.create_and_raise) + return d + +##METHODS_PART## + +class bridge: + + def __init__(self): + log.info("Init DBus...") + self._obj = DBusObject(const_OBJ_PATH) + + async def post_init(self): + try: + conn = await client.connect(reactor) + except error.DBusException as e: + if e.errName == "org.freedesktop.DBus.Error.NotSupported": + log.error( + _( + "D-Bus is not launched, please see README to see instructions on " + "how to launch it" + ) + ) + raise BridgeInitError(str(e)) + + conn.exportObject(self._obj) + await conn.requestBusName(const_INT_PREFIX) + +##SIGNALS_PART## + def register_method(self, name, callback): + log.debug(f"registering DBus bridge method [{name}]") + self._obj.register_method(name, callback) + + def emit_signal(self, name, *args): + self._obj.emitSignal(name, *args) + + def add_method( + self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={} + ): + """Dynamically add a method to D-Bus bridge""" + # FIXME: doc parameter is kept only temporary, the time to remove it from calls + log.debug(f"Adding method {name!r} to D-Bus bridge") + self._obj.plugin_iface.addMethod( + Method(name, arguments=in_sign, returns=out_sign) + ) + # we have to create a method here instead of using partialmethod, because txdbus + # uses __func__ which doesn't work with partialmethod + def caller(self_, *args, **kwargs): + return self_._callback(name, *args, **kwargs) + setattr(self._obj, f"dbus_{name}", MethodType(caller, self._obj)) + self.register_method(name, method) + + def add_signal(self, name, int_suffix, signature, doc={}): + """Dynamically add a signal to D-Bus bridge""" + log.debug(f"Adding signal {name!r} to D-Bus bridge") + self._obj.plugin_iface.addSignal(Signal(name, signature)) + setattr(bridge, name, partialmethod(bridge.emit_signal, name)) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +# SàT communication bridge +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +import dbus +import ast +from libervia.backend.core.i18n import _ +from libervia.backend.tools import config +from libervia.backend.core.log import getLogger +from libervia.backend.core.exceptions import BridgeExceptionNoService, BridgeInitError +from dbus.mainloop.glib import DBusGMainLoop +from .bridge_frontend import BridgeException + + +DBusGMainLoop(set_as_default=True) +log = getLogger(__name__) + + +# Interface prefix +const_INT_PREFIX = config.config_get( + config.parse_main_conf(), + "", + "bridge_dbus_int_prefix", + "org.libervia.Libervia") +const_ERROR_PREFIX = const_INT_PREFIX + ".error" +const_OBJ_PATH = '/org/libervia/Libervia/bridge' +const_CORE_SUFFIX = ".core" +const_PLUGIN_SUFFIX = ".plugin" +const_TIMEOUT = 120 + + +def dbus_to_bridge_exception(dbus_e): + """Convert a DBusException to a BridgeException. + + @param dbus_e (DBusException) + @return: BridgeException + """ + full_name = dbus_e.get_dbus_name() + if full_name.startswith(const_ERROR_PREFIX): + name = dbus_e.get_dbus_name()[len(const_ERROR_PREFIX) + 1:] + else: + name = full_name + # XXX: dbus_e.args doesn't contain the original DBusException args, but we + # receive its serialized form in dbus_e.args[0]. From that we can rebuild + # the original arguments list thanks to ast.literal_eval (secure eval). + message = dbus_e.get_dbus_message() # similar to dbus_e.args[0] + try: + message, condition = ast.literal_eval(message) + except (SyntaxError, ValueError, TypeError): + condition = '' + return BridgeException(name, message, condition) + + +class bridge: + + def bridge_connect(self, callback, errback): + try: + self.sessions_bus = dbus.SessionBus() + self.db_object = self.sessions_bus.get_object(const_INT_PREFIX, + const_OBJ_PATH) + self.db_core_iface = dbus.Interface(self.db_object, + dbus_interface=const_INT_PREFIX + const_CORE_SUFFIX) + self.db_plugin_iface = dbus.Interface(self.db_object, + dbus_interface=const_INT_PREFIX + const_PLUGIN_SUFFIX) + except dbus.exceptions.DBusException as e: + if e._dbus_error_name in ('org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Spawn.ExecFailed'): + errback(BridgeExceptionNoService()) + elif e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': + log.error(_("D-Bus is not launched, please see README to see instructions on how to launch it")) + errback(BridgeInitError) + else: + errback(e) + else: + callback() + #props = self.db_core_iface.getProperties() + + def register_signal(self, functionName, handler, iface="core"): + if iface == "core": + self.db_core_iface.connect_to_signal(functionName, handler) + elif iface == "plugin": + self.db_plugin_iface.connect_to_signal(functionName, handler) + else: + log.error(_('Unknown interface')) + + def __getattribute__(self, name): + """ usual __getattribute__ if the method exists, else try to find a plugin method """ + try: + return object.__getattribute__(self, name) + except AttributeError: + # The attribute is not found, we try the plugin proxy to find the requested method + + def get_plugin_method(*args, **kwargs): + # We first check if we have an async call. We detect this in two ways: + # - if we have the 'callback' and 'errback' keyword arguments + # - or if the last two arguments are callable + + async_ = False + args = list(args) + + if kwargs: + if 'callback' in kwargs: + async_ = True + _callback = kwargs.pop('callback') + _errback = kwargs.pop('errback', lambda failure: log.error(str(failure))) + try: + args.append(kwargs.pop('profile')) + except KeyError: + try: + args.append(kwargs.pop('profile_key')) + except KeyError: + pass + # at this point, kwargs should be empty + if kwargs: + log.warning("unexpected keyword arguments, they will be ignored: {}".format(kwargs)) + elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): + async_ = True + _errback = args.pop() + _callback = args.pop() + + method = getattr(self.db_plugin_iface, name) + + if async_: + kwargs['timeout'] = const_TIMEOUT + kwargs['reply_handler'] = _callback + kwargs['error_handler'] = lambda err: _errback(dbus_to_bridge_exception(err)) + + try: + return method(*args, **kwargs) + except ValueError as e: + if e.args[0].startswith("Unable to guess signature"): + # XXX: if frontend is started too soon after backend, the + # inspection misses methods (notably plugin dynamically added + # methods). The following hack works around that by redoing the + # cache of introspected methods signatures. + log.debug("using hack to work around inspection issue") + proxy = self.db_plugin_iface.proxy_object + IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS + proxy._introspect_state = IN_PROGRESS + proxy._Introspect() + return self.db_plugin_iface.get_dbus_method(name)(*args, **kwargs) + raise e + + return get_plugin_method + +##METHODS_PART## + +class AIOBridge(bridge): + + def register_signal(self, functionName, handler, iface="core"): + loop = asyncio.get_running_loop() + async_handler = lambda *args: asyncio.run_coroutine_threadsafe(handler(*args), loop) + return super().register_signal(functionName, async_handler, iface) + + def __getattribute__(self, name): + """ usual __getattribute__ if the method exists, else try to find a plugin method """ + try: + return object.__getattribute__(self, name) + except AttributeError: + # The attribute is not found, we try the plugin proxy to find the requested method + def get_plugin_method(*args, **kwargs): + loop = asyncio.get_running_loop() + fut = loop.create_future() + method = getattr(self.db_plugin_iface, name) + reply_handler = lambda ret=None: loop.call_soon_threadsafe( + fut.set_result, ret) + error_handler = lambda err: loop.call_soon_threadsafe( + fut.set_exception, dbus_to_bridge_exception(err)) + try: + method( + *args, + **kwargs, + timeout=const_TIMEOUT, + reply_handler=reply_handler, + error_handler=error_handler + ) + except ValueError as e: + if e.args[0].startswith("Unable to guess signature"): + # same hack as for bridge.__getattribute__ + log.warning("using hack to work around inspection issue") + proxy = self.db_plugin_iface.proxy_object + IN_PROGRESS = proxy.INTROSPECT_STATE_INTROSPECT_IN_PROGRESS + proxy._introspect_state = IN_PROGRESS + proxy._Introspect() + self.db_plugin_iface.get_dbus_method(name)( + *args, + **kwargs, + timeout=const_TIMEOUT, + reply_handler=reply_handler, + error_handler=error_handler + ) + + else: + raise e + return fut + + return get_plugin_method + + def bridge_connect(self): + loop = asyncio.get_running_loop() + fut = loop.create_future() + super().bridge_connect( + callback=lambda: loop.call_soon_threadsafe(fut.set_result, None), + errback=lambda e: loop.call_soon_threadsafe(fut.set_exception, e) + ) + return fut + +##ASYNC_METHODS_PART## diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/embedded/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/embedded/constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/embedded/constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.bridge_constructor import base_constructor + +#  from textwraps import dedent + + +class EmbeddedConstructor(base_constructor.Constructor): + NAME = "embedded" + CORE_TEMPLATE = "embedded_template.py" + CORE_DEST = "embedded.py" + CORE_FORMATS = { + "methods": """\ + def {name}(self, {args}{args_comma}callback=None, errback=None): +{ret_routine} +""", + "signals": """\ + def {name}(self, {args}): + try: + cb = self._signals_cbs["{category}"]["{name}"] + except KeyError: + log.warning(u"ignoring signal {name}: no callback registered") + else: + cb({args_result}) +""", + } + FRONTEND_TEMPLATE = "embedded_frontend_template.py" + FRONTEND_DEST = CORE_DEST + FRONTEND_FORMATS = {} + + def core_completion_method(self, completion, function, default, arg_doc, async_): + completion.update( + { + "debug": "" + if not self.args.debug + else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " "), + "args_result": self.get_arguments(function["sig_in"], name=arg_doc), + "args_comma": ", " if function["sig_in"] else "", + } + ) + + if async_: + completion["cb_or_lambda"] = ( + "callback" if function["sig_out"] else "lambda __: callback()" + ) + completion[ + "ret_routine" + ] = """\ + d = self._methods_cbs["{name}"]({args_result}) + if callback is not None: + d.addCallback({cb_or_lambda}) + if errback is None: + d.addErrback(lambda failure_: log.error(failure_)) + else: + d.addErrback(errback) + return d + """.format( + **completion + ) + else: + completion["ret_or_nothing"] = "ret" if function["sig_out"] else "" + completion[ + "ret_routine" + ] = """\ + try: + ret = self._methods_cbs["{name}"]({args_result}) + except Exception as e: + if errback is not None: + errback(e) + else: + raise e + else: + if callback is None: + return ret + else: + callback({ret_or_nothing})""".format( + **completion + ) + + def core_completion_signal(self, completion, function, default, arg_doc, async_): + completion.update( + {"args_result": self.get_arguments(function["sig_in"], name=arg_doc)} + ) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/embedded/embedded_frontend_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/embedded/embedded_frontend_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.embedded import bridge diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/embedded/embedded_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/embedded/embedded_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from libervia.backend.core import exceptions + + +class _Bridge(object): + def __init__(self): + log.debug("Init embedded bridge...") + self._methods_cbs = {} + self._signals_cbs = {"core": {}, "plugin": {}} + + def bridge_connect(self, callback, errback): + callback() + + def register_method(self, name, callback): + log.debug("registering embedded bridge method [{}]".format(name)) + if name in self._methods_cbs: + raise exceptions.ConflictError("method {} is already regitered".format(name)) + self._methods_cbs[name] = callback + + def register_signal(self, functionName, handler, iface="core"): + iface_dict = self._signals_cbs[iface] + if functionName in iface_dict: + raise exceptions.ConflictError( + "signal {name} is already regitered for interface {iface}".format( + name=functionName, iface=iface + ) + ) + iface_dict[functionName] = handler + + def call_method(self, name, out_sign, async_, args, kwargs): + callback = kwargs.pop("callback", None) + errback = kwargs.pop("errback", None) + if async_: + d = self._methods_cbs[name](*args, **kwargs) + if callback is not None: + d.addCallback(callback if out_sign else lambda __: callback()) + if errback is None: + d.addErrback(lambda failure_: log.error(failure_)) + else: + d.addErrback(errback) + return d + else: + try: + ret = self._methods_cbs[name](*args, **kwargs) + except Exception as e: + if errback is not None: + errback(e) + else: + raise e + else: + if callback is None: + return ret + else: + if out_sign: + callback(ret) + else: + callback() + + def send_signal(self, name, args, kwargs): + try: + cb = self._signals_cbs["plugin"][name] + except KeyError: + log.debug("ignoring signal {}: no callback registered".format(name)) + else: + cb(*args, **kwargs) + + def add_method(self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={}): + # FIXME: doc parameter is kept only temporary, the time to remove it from calls + log.debug("Adding method [{}] to embedded bridge".format(name)) + self.register_method(name, method) + setattr( + self.__class__, + name, + lambda self_, *args, **kwargs: self.call_method( + name, out_sign, async_, args, kwargs + ), + ) + + def add_signal(self, name, int_suffix, signature, doc={}): + setattr( + self.__class__, + name, + lambda self_, *args, **kwargs: self.send_signal(name, args, kwargs), + ) + + ## signals ## + + +##SIGNALS_PART## +## methods ## + +##METHODS_PART## + +# we want the same instance for both core and frontend +bridge = None + + +def bridge(): + global bridge + if bridge is None: + bridge = _Bridge() + return bridge diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/mediawiki/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/mediawiki/constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/mediawiki/constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.bridge_constructor import base_constructor +import sys +from datetime import datetime +import re + + +class MediawikiConstructor(base_constructor.Constructor): + def __init__(self, bridge_template, options): + base_constructor.Constructor.__init__(self, bridge_template, options) + self.core_template = "mediawiki_template.tpl" + self.core_dest = "mediawiki.wiki" + + def _add_text_decorations(self, text): + """Add text decorations like coloration or shortcuts""" + + def anchor_link(match): + link = match.group(1) + # we add anchor_link for [method_name] syntax: + if link in self.bridge_template.sections(): + return "[[#%s|%s]]" % (link, link) + print("WARNING: found an anchor link to an unknown method") + return link + + return re.sub(r"\[(\w+)\]", anchor_link, text) + + def _wiki_parameter(self, name, sig_in): + """Format parameters with the wiki syntax + @param name: name of the function + @param sig_in: signature in + @return: string of the formated parameters""" + arg_doc = self.get_arguments_doc(name) + arg_default = self.get_default(name) + args_str = self.get_arguments(sig_in) + args = args_str.split(", ") if args_str else [] # ugly but it works :) + wiki = [] + for i in range(len(args)): + if i in arg_doc: + name, doc = arg_doc[i] + doc = "\n:".join(doc.rstrip("\n").split("\n")) + wiki.append("; %s: %s" % (name, self._add_text_decorations(doc))) + else: + wiki.append("; arg_%d: " % i) + if i in arg_default: + wiki.append(":''DEFAULT: %s''" % arg_default[i]) + return "\n".join(wiki) + + def _wiki_return(self, name): + """Format return doc with the wiki syntax + @param name: name of the function + """ + arg_doc = self.get_arguments_doc(name) + wiki = [] + if "return" in arg_doc: + wiki.append("\n|-\n! scope=row | return value\n|") + wiki.append( + "
\n".join( + self._add_text_decorations(arg_doc["return"]).rstrip("\n").split("\n") + ) + ) + return "\n".join(wiki) + + def generate_core_side(self): + signals_part = [] + methods_part = [] + sections = self.bridge_template.sections() + sections.sort() + for section in sections: + function = self.getValues(section) + print(("Adding %s %s" % (section, function["type"]))) + async_msg = """
'''This method is asynchronous'''""" + deprecated_msg = """
'''/!\ WARNING /!\ : This method is deprecated, please don't use it !'''""" + signature_signal = ( + """\ +! scope=row | signature +| %s +|-\ +""" + % function["sig_in"] + ) + signature_method = """\ +! scope=row | signature in +| %s +|- +! scope=row | signature out +| %s +|-\ +""" % ( + function["sig_in"], + function["sig_out"], + ) + completion = { + "signature": signature_signal + if function["type"] == "signal" + else signature_method, + "sig_out": function["sig_out"] or "", + "category": function["category"], + "name": section, + "doc": self.get_doc(section) or "FIXME: No description available", + "async": async_msg if "async" in self.getFlags(section) else "", + "deprecated": deprecated_msg + if "deprecated" in self.getFlags(section) + else "", + "parameters": self._wiki_parameter(section, function["sig_in"]), + "return": self._wiki_return(section) + if function["type"] == "method" + else "", + } + + dest = signals_part if function["type"] == "signal" else methods_part + dest.append( + """\ +== %(name)s == +''%(doc)s'' +%(deprecated)s +%(async)s +{| class="wikitable" style="text-align:left; width:80%%;" +! scope=row | category +| %(category)s +|- +%(signature)s +! scope=row | parameters +| +%(parameters)s%(return)s +|} +""" + % completion + ) + + # at this point, signals_part, and methods_part should be filled, + # we just have to place them in the right part of the template + core_bridge = [] + template_path = self.get_template_path(self.core_template) + try: + with open(template_path) as core_template: + for line in core_template: + if line.startswith("##SIGNALS_PART##"): + core_bridge.extend(signals_part) + elif line.startswith("##METHODS_PART##"): + core_bridge.extend(methods_part) + elif line.startswith("##TIMESTAMP##"): + core_bridge.append("Generated on %s" % datetime.now()) + else: + core_bridge.append(line.replace("\n", "")) + except IOError: + print(("Can't open template file [%s]" % template_path)) + sys.exit(1) + + # now we write to final file + self.final_write(self.core_dest, core_bridge) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/mediawiki/mediawiki_template.tpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/mediawiki/mediawiki_template.tpl Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,11 @@ +[[Catégorie:Salut à Toi]] +[[Catégorie:documentation développeur]] + += Overview = +This is an autogenerated doc for SàT bridge's API += Signals = +##SIGNALS_PART## += Methods = +##METHODS_PART## +---- +##TIMESTAMP## diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/pb/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/pb/constructor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/constructor.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.bridge.bridge_constructor import base_constructor + + +class pbConstructor(base_constructor.Constructor): + NAME = "pb" + CORE_TEMPLATE = "pb_core_template.py" + CORE_DEST = "pb.py" + CORE_FORMATS = { + "signals": """\ + def {name}(self, {args}): + {debug}self.send_signal("{name}", {args_no_def})\n""" + } + + FRONTEND_TEMPLATE = "pb_frontend_template.py" + FRONTEND_DEST = CORE_DEST + FRONTEND_FORMATS = { + "methods": """\ + def {name}(self{args_comma}{args}, callback=None, errback=None): + {debug}d = self.root.callRemote("{name}"{args_comma}{args_no_def}) + if callback is not None: + d.addCallback({callback}) + if errback is None: + d.addErrback(self._generic_errback) + else: + d.addErrback(self._errback, ori_errback=errback)\n""", + "async_methods": """\ + def {name}(self{args_comma}{args}): + {debug}d = self.root.callRemote("{name}"{args_comma}{args_no_def}) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop())\n""", + } + + def core_completion_signal(self, completion, function, default, arg_doc, async_): + completion["args_no_def"] = self.get_arguments(function["sig_in"], name=arg_doc) + completion["debug"] = ( + "" + if not self.args.debug + else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " ") + ) + + def frontend_completion_method(self, completion, function, default, arg_doc, async_): + completion.update( + { + "args_comma": ", " if function["sig_in"] else "", + "args_no_def": self.get_arguments(function["sig_in"], name=arg_doc), + "callback": "callback" + if function["sig_out"] + else "lambda __: callback()", + "debug": "" + if not self.args.debug + else 'log.debug ("%s")\n%s' % (completion["name"], 8 * " "), + } + ) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_core_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import dataclasses +from functools import partial +from pathlib import Path +from twisted.spread import jelly, pb +from twisted.internet import reactor +from libervia.backend.core.log import getLogger +from libervia.backend.tools import config + +log = getLogger(__name__) + + +## jelly hack +# we monkey patch jelly to handle namedtuple +ori_jelly = jelly._Jellier.jelly + + +def fixed_jelly(self, obj): + """this method fix handling of namedtuple""" + if isinstance(obj, tuple) and not obj is tuple: + obj = tuple(obj) + return ori_jelly(self, obj) + + +jelly._Jellier.jelly = fixed_jelly + + +@dataclasses.dataclass(eq=False) +class HandlerWrapper: + # we use a wrapper to keep signals handlers because RemoteReference doesn't support + # comparison (other than equality), making it unusable with a list + handler: pb.RemoteReference + + +class PBRoot(pb.Root): + def __init__(self): + self.signals_handlers = [] + + def remote_init_bridge(self, signals_handler): + self.signals_handlers.append(HandlerWrapper(signals_handler)) + log.info("registered signal handler") + + def send_signal_eb(self, failure_, signal_name): + if not failure_.check(pb.PBConnectionLost): + log.error( + f"Error while sending signal {signal_name}: {failure_}", + ) + + def send_signal(self, name, args, kwargs): + to_remove = [] + for wrapper in self.signals_handlers: + handler = wrapper.handler + try: + d = handler.callRemote(name, *args, **kwargs) + except pb.DeadReferenceError: + to_remove.append(wrapper) + else: + d.addErrback(self.send_signal_eb, name) + if to_remove: + for wrapper in to_remove: + log.debug("Removing signal handler for dead frontend") + self.signals_handlers.remove(wrapper) + + def _bridge_deactivate_signals(self): + if hasattr(self, "signals_paused"): + log.warning("bridge signals already deactivated") + if self.signals_handler: + self.signals_paused.extend(self.signals_handler) + else: + self.signals_paused = self.signals_handlers + self.signals_handlers = [] + log.debug("bridge signals have been deactivated") + + def _bridge_reactivate_signals(self): + try: + self.signals_handlers = self.signals_paused + except AttributeError: + log.debug("signals were already activated") + else: + del self.signals_paused + log.debug("bridge signals have been reactivated") + +##METHODS_PART## + + +class bridge(object): + def __init__(self): + log.info("Init Perspective Broker...") + self.root = PBRoot() + conf = config.parse_main_conf() + get_conf = partial(config.get_conf, conf, "bridge_pb", "") + conn_type = get_conf("connection_type", "unix_socket") + if conn_type == "unix_socket": + local_dir = Path(config.config_get(conf, "", "local_dir")).resolve() + socket_path = local_dir / "bridge_pb" + log.info(f"using UNIX Socket at {socket_path}") + reactor.listenUNIX( + str(socket_path), pb.PBServerFactory(self.root), mode=0o600 + ) + elif conn_type == "socket": + port = int(get_conf("port", 8789)) + log.info(f"using TCP Socket at port {port}") + reactor.listenTCP(port, pb.PBServerFactory(self.root)) + else: + raise ValueError(f"Unknown pb connection type: {conn_type!r}") + + def send_signal(self, name, *args, **kwargs): + self.root.send_signal(name, args, kwargs) + + def remote_init_bridge(self, signals_handler): + self.signals_handlers.append(signals_handler) + log.info("registered signal handler") + + def register_method(self, name, callback): + log.debug("registering PB bridge method [%s]" % name) + setattr(self.root, "remote_" + name, callback) + #  self.root.register_method(name, callback) + + def add_method( + self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={} + ): + """Dynamically add a method to PB bridge""" + # FIXME: doc parameter is kept only temporary, the time to remove it from calls + log.debug("Adding method {name} to PB bridge".format(name=name)) + self.register_method(name, method) + + def add_signal(self, name, int_suffix, signature, doc={}): + log.debug("Adding signal {name} to PB bridge".format(name=name)) + setattr( + self, name, lambda *args, **kwargs: self.send_signal(name, *args, **kwargs) + ) + + def bridge_deactivate_signals(self): + """Stop sending signals to bridge + + Mainly used for mobile frontends, when the frontend is paused + """ + self.root._bridge_deactivate_signals() + + def bridge_reactivate_signals(self): + """Send again signals to bridge + + Should only be used after bridge_deactivate_signals has been called + """ + self.root._bridge_reactivate_signals() + +##SIGNALS_PART## diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 + +# SàT communication bridge +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +from logging import getLogger +from functools import partial +from pathlib import Path +from twisted.spread import pb +from twisted.internet import reactor, defer +from twisted.internet.error import ConnectionRefusedError, ConnectError +from libervia.backend.core import exceptions +from libervia.backend.tools import config +from sat_frontends.bridge.bridge_frontend import BridgeException + +log = getLogger(__name__) + + +class SignalsHandler(pb.Referenceable): + def __getattr__(self, name): + if name.startswith("remote_"): + log.debug("calling an unregistered signal: {name}".format(name=name[7:])) + return lambda *args, **kwargs: None + + else: + raise AttributeError(name) + + def register_signal(self, name, handler, iface="core"): + log.debug("registering signal {name}".format(name=name)) + method_name = "remote_" + name + try: + self.__getattribute__(method_name) + except AttributeError: + pass + else: + raise exceptions.InternalError( + "{name} signal handler has been registered twice".format( + name=method_name + ) + ) + setattr(self, method_name, handler) + + +class bridge(object): + + def __init__(self): + self.signals_handler = SignalsHandler() + + def __getattr__(self, name): + return partial(self.call, name) + + def _generic_errback(self, err): + log.error(f"bridge error: {err}") + + def _errback(self, failure_, ori_errback): + """Convert Failure to BridgeException""" + ori_errback( + BridgeException( + name=failure_.type.decode('utf-8'), + message=str(failure_.value) + ) + ) + + def remote_callback(self, result, callback): + """call callback with argument or None + + if result is not None not argument is used, + else result is used as argument + @param result: remote call result + @param callback(callable): method to call on result + """ + if result is None: + callback() + else: + callback(result) + + def call(self, name, *args, **kwargs): + """call a remote method + + @param name(str): name of the bridge method + @param args(list): arguments + may contain callback and errback as last 2 items + @param kwargs(dict): keyword arguments + may contain callback and errback + """ + callback = errback = None + if kwargs: + try: + callback = kwargs.pop("callback") + except KeyError: + pass + try: + errback = kwargs.pop("errback") + except KeyError: + pass + elif len(args) >= 2 and callable(args[-1]) and callable(args[-2]): + errback = args.pop() + callback = args.pop() + d = self.root.callRemote(name, *args, **kwargs) + if callback is not None: + d.addCallback(self.remote_callback, callback) + if errback is not None: + d.addErrback(errback) + + def _init_bridge_eb(self, failure_): + log.error("Can't init bridge: {msg}".format(msg=failure_)) + return failure_ + + def _set_root(self, root): + """set remote root object + + bridge will then be initialised + """ + self.root = root + d = root.callRemote("initBridge", self.signals_handler) + d.addErrback(self._init_bridge_eb) + return d + + def get_root_object_eb(self, failure_): + """Call errback with appropriate bridge error""" + if failure_.check(ConnectionRefusedError, ConnectError): + raise exceptions.BridgeExceptionNoService + else: + raise failure_ + + def bridge_connect(self, callback, errback): + factory = pb.PBClientFactory() + conf = config.parse_main_conf() + get_conf = partial(config.get_conf, conf, "bridge_pb", "") + conn_type = get_conf("connection_type", "unix_socket") + if conn_type == "unix_socket": + local_dir = Path(config.config_get(conf, "", "local_dir")).resolve() + socket_path = local_dir / "bridge_pb" + reactor.connectUNIX(str(socket_path), factory) + elif conn_type == "socket": + host = get_conf("host", "localhost") + port = int(get_conf("port", 8789)) + reactor.connectTCP(host, port, factory) + else: + raise ValueError(f"Unknown pb connection type: {conn_type!r}") + d = factory.getRootObject() + d.addCallback(self._set_root) + if callback is not None: + d.addCallback(lambda __: callback()) + d.addErrback(self.get_root_object_eb) + if errback is not None: + d.addErrback(lambda failure_: errback(failure_.value)) + return d + + def register_signal(self, functionName, handler, iface="core"): + self.signals_handler.register_signal(functionName, handler, iface) + + +##METHODS_PART## + +class AIOSignalsHandler(SignalsHandler): + + def register_signal(self, name, handler, iface="core"): + async_handler = lambda *args, **kwargs: defer.Deferred.fromFuture( + asyncio.ensure_future(handler(*args, **kwargs))) + return super().register_signal(name, async_handler, iface) + + +class AIOBridge(bridge): + + def __init__(self): + self.signals_handler = AIOSignalsHandler() + + def _errback(self, failure_): + """Convert Failure to BridgeException""" + raise BridgeException( + name=failure_.type.decode('utf-8'), + message=str(failure_.value) + ) + + def call(self, name, *args, **kwargs): + d = self.root.callRemote(name, *args, *kwargs) + d.addErrback(self._errback) + return d.asFuture(asyncio.get_event_loop()) + + async def bridge_connect(self): + d = super().bridge_connect(callback=None, errback=None) + return await d.asFuture(asyncio.get_event_loop()) + +##ASYNC_METHODS_PART## diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/dbus_bridge.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/dbus_bridge.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,495 @@ +#!/usr/bin/env python3 + +# Libervia communication bridge +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from types import MethodType +from functools import partialmethod +from twisted.internet import defer, reactor +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.core.exceptions import BridgeInitError +from libervia.backend.tools import config +from txdbus import client, objects, error +from txdbus.interface import DBusInterface, Method, Signal + + +log = getLogger(__name__) + +# Interface prefix +const_INT_PREFIX = config.config_get( + config.parse_main_conf(), + "", + "bridge_dbus_int_prefix", + "org.libervia.Libervia") +const_ERROR_PREFIX = const_INT_PREFIX + ".error" +const_OBJ_PATH = "/org/libervia/Libervia/bridge" +const_CORE_SUFFIX = ".core" +const_PLUGIN_SUFFIX = ".plugin" + + +class ParseError(Exception): + pass + + +class DBusException(Exception): + pass + + +class MethodNotRegistered(DBusException): + dbusErrorName = const_ERROR_PREFIX + ".MethodNotRegistered" + + +class GenericException(DBusException): + def __init__(self, twisted_error): + """ + + @param twisted_error (Failure): instance of twisted Failure + error message is used to store a repr of message and condition in a tuple, + so it can be evaluated by the frontend bridge. + """ + try: + # twisted_error.value is a class + class_ = twisted_error.value().__class__ + except TypeError: + # twisted_error.value is an instance + class_ = twisted_error.value.__class__ + data = twisted_error.getErrorMessage() + try: + data = (data, twisted_error.value.condition) + except AttributeError: + data = (data,) + else: + data = (str(twisted_error),) + self.dbusErrorName = ".".join( + (const_ERROR_PREFIX, class_.__module__, class_.__name__) + ) + super(GenericException, self).__init__(repr(data)) + + @classmethod + def create_and_raise(cls, exc): + raise cls(exc) + + +class DBusObject(objects.DBusObject): + + core_iface = DBusInterface( + const_INT_PREFIX + const_CORE_SUFFIX, + Method('action_launch', arguments='sss', returns='s'), + Method('actions_get', arguments='s', returns='a(ssi)'), + Method('config_get', arguments='ss', returns='s'), + Method('connect', arguments='ssa{ss}', returns='b'), + Method('contact_add', arguments='ss', returns=''), + Method('contact_del', arguments='ss', returns=''), + Method('contact_get', arguments='ss', returns='(a{ss}as)'), + Method('contact_update', arguments='ssass', returns=''), + Method('contacts_get', arguments='s', returns='a(sa{ss}as)'), + Method('contacts_get_from_group', arguments='ss', returns='as'), + Method('devices_infos_get', arguments='ss', returns='s'), + Method('disco_find_by_features', arguments='asa(ss)bbbbbs', returns='(a{sa(sss)}a{sa(sss)}a{sa(sss)})'), + Method('disco_infos', arguments='ssbs', returns='(asa(sss)a{sa(a{ss}as)})'), + Method('disco_items', arguments='ssbs', returns='a(sss)'), + Method('disconnect', arguments='s', returns=''), + Method('encryption_namespace_get', arguments='s', returns='s'), + Method('encryption_plugins_get', arguments='', returns='s'), + Method('encryption_trust_ui_get', arguments='sss', returns='s'), + Method('entities_data_get', arguments='asass', returns='a{sa{ss}}'), + Method('entity_data_get', arguments='sass', returns='a{ss}'), + Method('features_get', arguments='s', returns='a{sa{ss}}'), + Method('history_get', arguments='ssiba{ss}s', returns='a(sdssa{ss}a{ss}ss)'), + Method('image_check', arguments='s', returns='s'), + Method('image_convert', arguments='ssss', returns='s'), + Method('image_generate_preview', arguments='ss', returns='s'), + Method('image_resize', arguments='sii', returns='s'), + Method('is_connected', arguments='s', returns='b'), + Method('main_resource_get', arguments='ss', returns='s'), + Method('menu_help_get', arguments='ss', returns='s'), + Method('menu_launch', arguments='sasa{ss}is', returns='a{ss}'), + Method('menus_get', arguments='si', returns='a(ssasasa{ss})'), + Method('message_encryption_get', arguments='ss', returns='s'), + Method('message_encryption_start', arguments='ssbs', returns=''), + Method('message_encryption_stop', arguments='ss', returns=''), + Method('message_send', arguments='sa{ss}a{ss}sss', returns=''), + Method('namespaces_get', arguments='', returns='a{ss}'), + Method('param_get_a', arguments='ssss', returns='s'), + Method('param_get_a_async', arguments='sssis', returns='s'), + Method('param_set', arguments='sssis', returns=''), + Method('param_ui_get', arguments='isss', returns='s'), + Method('params_categories_get', arguments='', returns='as'), + Method('params_register_app', arguments='sis', returns=''), + Method('params_template_load', arguments='s', returns='b'), + Method('params_template_save', arguments='s', returns='b'), + Method('params_values_from_category_get_async', arguments='sisss', returns='a{ss}'), + Method('presence_set', arguments='ssa{ss}s', returns=''), + Method('presence_statuses_get', arguments='s', returns='a{sa{s(sia{ss})}}'), + Method('private_data_delete', arguments='sss', returns=''), + Method('private_data_get', arguments='sss', returns='s'), + Method('private_data_set', arguments='ssss', returns=''), + Method('profile_create', arguments='sss', returns=''), + Method('profile_delete_async', arguments='s', returns=''), + Method('profile_is_session_started', arguments='s', returns='b'), + Method('profile_name_get', arguments='s', returns='s'), + Method('profile_set_default', arguments='s', returns=''), + Method('profile_start_session', arguments='ss', returns='b'), + Method('profiles_list_get', arguments='bb', returns='as'), + Method('progress_get', arguments='ss', returns='a{ss}'), + Method('progress_get_all', arguments='s', returns='a{sa{sa{ss}}}'), + Method('progress_get_all_metadata', arguments='s', returns='a{sa{sa{ss}}}'), + Method('ready_get', arguments='', returns=''), + Method('roster_resync', arguments='s', returns=''), + Method('session_infos_get', arguments='s', returns='a{ss}'), + Method('sub_waiting_get', arguments='s', returns='a{ss}'), + Method('subscription', arguments='sss', returns=''), + Method('version_get', arguments='', returns='s'), + Signal('_debug', 'sa{ss}s'), + Signal('action_new', 'ssis'), + Signal('connected', 'ss'), + Signal('contact_deleted', 'ss'), + Signal('contact_new', 'sa{ss}ass'), + Signal('disconnected', 's'), + Signal('entity_data_updated', 'ssss'), + Signal('message_encryption_started', 'sss'), + Signal('message_encryption_stopped', 'sa{ss}s'), + Signal('message_new', 'sdssa{ss}a{ss}sss'), + Signal('param_update', 'ssss'), + Signal('presence_update', 'ssia{ss}s'), + Signal('progress_error', 'sss'), + Signal('progress_finished', 'sa{ss}s'), + Signal('progress_started', 'sa{ss}s'), + Signal('subscribe', 'sss'), + ) + plugin_iface = DBusInterface( + const_INT_PREFIX + const_PLUGIN_SUFFIX + ) + + dbusInterfaces = [core_iface, plugin_iface] + + def __init__(self, path): + super().__init__(path) + log.debug("Init DBusObject...") + self.cb = {} + + def register_method(self, name, cb): + self.cb[name] = cb + + def _callback(self, name, *args, **kwargs): + """Call the callback if it exists, raise an exception else""" + try: + cb = self.cb[name] + except KeyError: + raise MethodNotRegistered + else: + d = defer.maybeDeferred(cb, *args, **kwargs) + d.addErrback(GenericException.create_and_raise) + return d + + def dbus_action_launch(self, callback_id, data, profile_key="@DEFAULT@"): + return self._callback("action_launch", callback_id, data, profile_key) + + def dbus_actions_get(self, profile_key="@DEFAULT@"): + return self._callback("actions_get", profile_key) + + def dbus_config_get(self, section, name): + return self._callback("config_get", section, name) + + def dbus_connect(self, profile_key="@DEFAULT@", password='', options={}): + return self._callback("connect", profile_key, password, options) + + def dbus_contact_add(self, entity_jid, profile_key="@DEFAULT@"): + return self._callback("contact_add", entity_jid, profile_key) + + def dbus_contact_del(self, entity_jid, profile_key="@DEFAULT@"): + return self._callback("contact_del", entity_jid, profile_key) + + def dbus_contact_get(self, arg_0, profile_key="@DEFAULT@"): + return self._callback("contact_get", arg_0, profile_key) + + def dbus_contact_update(self, entity_jid, name, groups, profile_key="@DEFAULT@"): + return self._callback("contact_update", entity_jid, name, groups, profile_key) + + def dbus_contacts_get(self, profile_key="@DEFAULT@"): + return self._callback("contacts_get", profile_key) + + def dbus_contacts_get_from_group(self, group, profile_key="@DEFAULT@"): + return self._callback("contacts_get_from_group", group, profile_key) + + def dbus_devices_infos_get(self, bare_jid, profile_key): + return self._callback("devices_infos_get", bare_jid, profile_key) + + def dbus_disco_find_by_features(self, namespaces, identities, bare_jid=False, service=True, roster=True, own_jid=True, local_device=False, profile_key="@DEFAULT@"): + return self._callback("disco_find_by_features", namespaces, identities, bare_jid, service, roster, own_jid, local_device, profile_key) + + def dbus_disco_infos(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + return self._callback("disco_infos", entity_jid, node, use_cache, profile_key) + + def dbus_disco_items(self, entity_jid, node=u'', use_cache=True, profile_key="@DEFAULT@"): + return self._callback("disco_items", entity_jid, node, use_cache, profile_key) + + def dbus_disconnect(self, profile_key="@DEFAULT@"): + return self._callback("disconnect", profile_key) + + def dbus_encryption_namespace_get(self, arg_0): + return self._callback("encryption_namespace_get", arg_0) + + def dbus_encryption_plugins_get(self, ): + return self._callback("encryption_plugins_get", ) + + def dbus_encryption_trust_ui_get(self, to_jid, namespace, profile_key): + return self._callback("encryption_trust_ui_get", to_jid, namespace, profile_key) + + def dbus_entities_data_get(self, jids, keys, profile): + return self._callback("entities_data_get", jids, keys, profile) + + def dbus_entity_data_get(self, jid, keys, profile): + return self._callback("entity_data_get", jid, keys, profile) + + def dbus_features_get(self, profile_key): + return self._callback("features_get", profile_key) + + def dbus_history_get(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@"): + return self._callback("history_get", from_jid, to_jid, limit, between, filters, profile) + + def dbus_image_check(self, arg_0): + return self._callback("image_check", arg_0) + + def dbus_image_convert(self, source, dest, arg_2, extra): + return self._callback("image_convert", source, dest, arg_2, extra) + + def dbus_image_generate_preview(self, image_path, profile_key): + return self._callback("image_generate_preview", image_path, profile_key) + + def dbus_image_resize(self, image_path, width, height): + return self._callback("image_resize", image_path, width, height) + + def dbus_is_connected(self, profile_key="@DEFAULT@"): + return self._callback("is_connected", profile_key) + + def dbus_main_resource_get(self, contact_jid, profile_key="@DEFAULT@"): + return self._callback("main_resource_get", contact_jid, profile_key) + + def dbus_menu_help_get(self, menu_id, language): + return self._callback("menu_help_get", menu_id, language) + + def dbus_menu_launch(self, menu_type, path, data, security_limit, profile_key): + return self._callback("menu_launch", menu_type, path, data, security_limit, profile_key) + + def dbus_menus_get(self, language, security_limit): + return self._callback("menus_get", language, security_limit) + + def dbus_message_encryption_get(self, to_jid, profile_key): + return self._callback("message_encryption_get", to_jid, profile_key) + + def dbus_message_encryption_start(self, to_jid, namespace='', replace=False, profile_key="@NONE@"): + return self._callback("message_encryption_start", to_jid, namespace, replace, profile_key) + + def dbus_message_encryption_stop(self, to_jid, profile_key): + return self._callback("message_encryption_stop", to_jid, profile_key) + + def dbus_message_send(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@"): + return self._callback("message_send", to_jid, message, subject, mess_type, extra, profile_key) + + def dbus_namespaces_get(self, ): + return self._callback("namespaces_get", ) + + def dbus_param_get_a(self, name, category, attribute="value", profile_key="@DEFAULT@"): + return self._callback("param_get_a", name, category, attribute, profile_key) + + def dbus_param_get_a_async(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@"): + return self._callback("param_get_a_async", name, category, attribute, security_limit, profile_key) + + def dbus_param_set(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): + return self._callback("param_set", name, value, category, security_limit, profile_key) + + def dbus_param_ui_get(self, security_limit=-1, app='', extra='', profile_key="@DEFAULT@"): + return self._callback("param_ui_get", security_limit, app, extra, profile_key) + + def dbus_params_categories_get(self, ): + return self._callback("params_categories_get", ) + + def dbus_params_register_app(self, xml, security_limit=-1, app=''): + return self._callback("params_register_app", xml, security_limit, app) + + def dbus_params_template_load(self, filename): + return self._callback("params_template_load", filename) + + def dbus_params_template_save(self, filename): + return self._callback("params_template_save", filename) + + def dbus_params_values_from_category_get_async(self, category, security_limit=-1, app="", extra="", profile_key="@DEFAULT@"): + return self._callback("params_values_from_category_get_async", category, security_limit, app, extra, profile_key) + + def dbus_presence_set(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): + return self._callback("presence_set", to_jid, show, statuses, profile_key) + + def dbus_presence_statuses_get(self, profile_key="@DEFAULT@"): + return self._callback("presence_statuses_get", profile_key) + + def dbus_private_data_delete(self, namespace, key, arg_2): + return self._callback("private_data_delete", namespace, key, arg_2) + + def dbus_private_data_get(self, namespace, key, profile_key): + return self._callback("private_data_get", namespace, key, profile_key) + + def dbus_private_data_set(self, namespace, key, data, profile_key): + return self._callback("private_data_set", namespace, key, data, profile_key) + + def dbus_profile_create(self, profile, password='', component=''): + return self._callback("profile_create", profile, password, component) + + def dbus_profile_delete_async(self, profile): + return self._callback("profile_delete_async", profile) + + def dbus_profile_is_session_started(self, profile_key="@DEFAULT@"): + return self._callback("profile_is_session_started", profile_key) + + def dbus_profile_name_get(self, profile_key="@DEFAULT@"): + return self._callback("profile_name_get", profile_key) + + def dbus_profile_set_default(self, profile): + return self._callback("profile_set_default", profile) + + def dbus_profile_start_session(self, password='', profile_key="@DEFAULT@"): + return self._callback("profile_start_session", password, profile_key) + + def dbus_profiles_list_get(self, clients=True, components=False): + return self._callback("profiles_list_get", clients, components) + + def dbus_progress_get(self, id, profile): + return self._callback("progress_get", id, profile) + + def dbus_progress_get_all(self, profile): + return self._callback("progress_get_all", profile) + + def dbus_progress_get_all_metadata(self, profile): + return self._callback("progress_get_all_metadata", profile) + + def dbus_ready_get(self, ): + return self._callback("ready_get", ) + + def dbus_roster_resync(self, profile_key="@DEFAULT@"): + return self._callback("roster_resync", profile_key) + + def dbus_session_infos_get(self, profile_key): + return self._callback("session_infos_get", profile_key) + + def dbus_sub_waiting_get(self, profile_key="@DEFAULT@"): + return self._callback("sub_waiting_get", profile_key) + + def dbus_subscription(self, sub_type, entity, profile_key="@DEFAULT@"): + return self._callback("subscription", sub_type, entity, profile_key) + + def dbus_version_get(self, ): + return self._callback("version_get", ) + + +class bridge: + + def __init__(self): + log.info("Init DBus...") + self._obj = DBusObject(const_OBJ_PATH) + + async def post_init(self): + try: + conn = await client.connect(reactor) + except error.DBusException as e: + if e.errName == "org.freedesktop.DBus.Error.NotSupported": + log.error( + _( + "D-Bus is not launched, please see README to see instructions on " + "how to launch it" + ) + ) + raise BridgeInitError(str(e)) + + conn.exportObject(self._obj) + await conn.requestBusName(const_INT_PREFIX) + + def _debug(self, action, params, profile): + self._obj.emitSignal("_debug", action, params, profile) + + def action_new(self, action_data, id, security_limit, profile): + self._obj.emitSignal("action_new", action_data, id, security_limit, profile) + + def connected(self, jid_s, profile): + self._obj.emitSignal("connected", jid_s, profile) + + def contact_deleted(self, entity_jid, profile): + self._obj.emitSignal("contact_deleted", entity_jid, profile) + + def contact_new(self, contact_jid, attributes, groups, profile): + self._obj.emitSignal("contact_new", contact_jid, attributes, groups, profile) + + def disconnected(self, profile): + self._obj.emitSignal("disconnected", profile) + + def entity_data_updated(self, jid, name, value, profile): + self._obj.emitSignal("entity_data_updated", jid, name, value, profile) + + def message_encryption_started(self, to_jid, encryption_data, profile_key): + self._obj.emitSignal("message_encryption_started", to_jid, encryption_data, profile_key) + + def message_encryption_stopped(self, to_jid, encryption_data, profile_key): + self._obj.emitSignal("message_encryption_stopped", to_jid, encryption_data, profile_key) + + def message_new(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): + self._obj.emitSignal("message_new", uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile) + + def param_update(self, name, value, category, profile): + self._obj.emitSignal("param_update", name, value, category, profile) + + def presence_update(self, entity_jid, show, priority, statuses, profile): + self._obj.emitSignal("presence_update", entity_jid, show, priority, statuses, profile) + + def progress_error(self, id, error, profile): + self._obj.emitSignal("progress_error", id, error, profile) + + def progress_finished(self, id, metadata, profile): + self._obj.emitSignal("progress_finished", id, metadata, profile) + + def progress_started(self, id, metadata, profile): + self._obj.emitSignal("progress_started", id, metadata, profile) + + def subscribe(self, sub_type, entity_jid, profile): + self._obj.emitSignal("subscribe", sub_type, entity_jid, profile) + + def register_method(self, name, callback): + log.debug(f"registering DBus bridge method [{name}]") + self._obj.register_method(name, callback) + + def emit_signal(self, name, *args): + self._obj.emitSignal(name, *args) + + def add_method( + self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={} + ): + """Dynamically add a method to D-Bus bridge""" + # FIXME: doc parameter is kept only temporary, the time to remove it from calls + log.debug(f"Adding method {name!r} to D-Bus bridge") + self._obj.plugin_iface.addMethod( + Method(name, arguments=in_sign, returns=out_sign) + ) + # we have to create a method here instead of using partialmethod, because txdbus + # uses __func__ which doesn't work with partialmethod + def caller(self_, *args, **kwargs): + return self_._callback(name, *args, **kwargs) + setattr(self._obj, f"dbus_{name}", MethodType(caller, self._obj)) + self.register_method(name, method) + + def add_signal(self, name, int_suffix, signature, doc={}): + """Dynamically add a signal to D-Bus bridge""" + log.debug(f"Adding signal {name!r} to D-Bus bridge") + self._obj.plugin_iface.addSignal(Signal(name, signature)) + setattr(bridge, name, partialmethod(bridge.emit_signal, name)) \ No newline at end of file diff -r d10748475025 -r 4b842c1fb686 libervia/backend/bridge/pb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/bridge/pb.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import dataclasses +from functools import partial +from pathlib import Path +from twisted.spread import jelly, pb +from twisted.internet import reactor +from libervia.backend.core.log import getLogger +from libervia.backend.tools import config + +log = getLogger(__name__) + + +## jelly hack +# we monkey patch jelly to handle namedtuple +ori_jelly = jelly._Jellier.jelly + + +def fixed_jelly(self, obj): + """this method fix handling of namedtuple""" + if isinstance(obj, tuple) and not obj is tuple: + obj = tuple(obj) + return ori_jelly(self, obj) + + +jelly._Jellier.jelly = fixed_jelly + + +@dataclasses.dataclass(eq=False) +class HandlerWrapper: + # we use a wrapper to keep signals handlers because RemoteReference doesn't support + # comparison (other than equality), making it unusable with a list + handler: pb.RemoteReference + + +class PBRoot(pb.Root): + def __init__(self): + self.signals_handlers = [] + + def remote_init_bridge(self, signals_handler): + self.signals_handlers.append(HandlerWrapper(signals_handler)) + log.info("registered signal handler") + + def send_signal_eb(self, failure_, signal_name): + if not failure_.check(pb.PBConnectionLost): + log.error( + f"Error while sending signal {signal_name}: {failure_}", + ) + + def send_signal(self, name, args, kwargs): + to_remove = [] + for wrapper in self.signals_handlers: + handler = wrapper.handler + try: + d = handler.callRemote(name, *args, **kwargs) + except pb.DeadReferenceError: + to_remove.append(wrapper) + else: + d.addErrback(self.send_signal_eb, name) + if to_remove: + for wrapper in to_remove: + log.debug("Removing signal handler for dead frontend") + self.signals_handlers.remove(wrapper) + + def _bridge_deactivate_signals(self): + if hasattr(self, "signals_paused"): + log.warning("bridge signals already deactivated") + if self.signals_handler: + self.signals_paused.extend(self.signals_handler) + else: + self.signals_paused = self.signals_handlers + self.signals_handlers = [] + log.debug("bridge signals have been deactivated") + + def _bridge_reactivate_signals(self): + try: + self.signals_handlers = self.signals_paused + except AttributeError: + log.debug("signals were already activated") + else: + del self.signals_paused + log.debug("bridge signals have been reactivated") + +##METHODS_PART## + + +class bridge(object): + def __init__(self): + log.info("Init Perspective Broker...") + self.root = PBRoot() + conf = config.parse_main_conf() + get_conf = partial(config.get_conf, conf, "bridge_pb", "") + conn_type = get_conf("connection_type", "unix_socket") + if conn_type == "unix_socket": + local_dir = Path(config.config_get(conf, "", "local_dir")).resolve() + socket_path = local_dir / "bridge_pb" + log.info(f"using UNIX Socket at {socket_path}") + reactor.listenUNIX( + str(socket_path), pb.PBServerFactory(self.root), mode=0o600 + ) + elif conn_type == "socket": + port = int(get_conf("port", 8789)) + log.info(f"using TCP Socket at port {port}") + reactor.listenTCP(port, pb.PBServerFactory(self.root)) + else: + raise ValueError(f"Unknown pb connection type: {conn_type!r}") + + def send_signal(self, name, *args, **kwargs): + self.root.send_signal(name, args, kwargs) + + def remote_init_bridge(self, signals_handler): + self.signals_handlers.append(signals_handler) + log.info("registered signal handler") + + def register_method(self, name, callback): + log.debug("registering PB bridge method [%s]" % name) + setattr(self.root, "remote_" + name, callback) + #  self.root.register_method(name, callback) + + def add_method( + self, name, int_suffix, in_sign, out_sign, method, async_=False, doc={} + ): + """Dynamically add a method to PB bridge""" + # FIXME: doc parameter is kept only temporary, the time to remove it from calls + log.debug("Adding method {name} to PB bridge".format(name=name)) + self.register_method(name, method) + + def add_signal(self, name, int_suffix, signature, doc={}): + log.debug("Adding signal {name} to PB bridge".format(name=name)) + setattr( + self, name, lambda *args, **kwargs: self.send_signal(name, *args, **kwargs) + ) + + def bridge_deactivate_signals(self): + """Stop sending signals to bridge + + Mainly used for mobile frontends, when the frontend is paused + """ + self.root._bridge_deactivate_signals() + + def bridge_reactivate_signals(self): + """Send again signals to bridge + + Should only be used after bridge_deactivate_signals has been called + """ + self.root._bridge_reactivate_signals() + + def _debug(self, action, params, profile): + self.send_signal("_debug", action, params, profile) + + def action_new(self, action_data, id, security_limit, profile): + self.send_signal("action_new", action_data, id, security_limit, profile) + + def connected(self, jid_s, profile): + self.send_signal("connected", jid_s, profile) + + def contact_deleted(self, entity_jid, profile): + self.send_signal("contact_deleted", entity_jid, profile) + + def contact_new(self, contact_jid, attributes, groups, profile): + self.send_signal("contact_new", contact_jid, attributes, groups, profile) + + def disconnected(self, profile): + self.send_signal("disconnected", profile) + + def entity_data_updated(self, jid, name, value, profile): + self.send_signal("entity_data_updated", jid, name, value, profile) + + def message_encryption_started(self, to_jid, encryption_data, profile_key): + self.send_signal("message_encryption_started", to_jid, encryption_data, profile_key) + + def message_encryption_stopped(self, to_jid, encryption_data, profile_key): + self.send_signal("message_encryption_stopped", to_jid, encryption_data, profile_key) + + def message_new(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): + self.send_signal("message_new", uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile) + + def param_update(self, name, value, category, profile): + self.send_signal("param_update", name, value, category, profile) + + def presence_update(self, entity_jid, show, priority, statuses, profile): + self.send_signal("presence_update", entity_jid, show, priority, statuses, profile) + + def progress_error(self, id, error, profile): + self.send_signal("progress_error", id, error, profile) + + def progress_finished(self, id, metadata, profile): + self.send_signal("progress_finished", id, metadata, profile) + + def progress_started(self, id, metadata, profile): + self.send_signal("progress_started", id, metadata, profile) + + def subscribe(self, sub_type, entity_jid, profile): + self.send_signal("subscribe", sub_type, entity_jid, profile) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/constants.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +try: + from xdg import BaseDirectory + from os.path import expanduser, realpath +except ImportError: + BaseDirectory = None +from os.path import dirname +from typing import Final +from libervia import backend + + +class Const(object): + + ## Application ## + APP_NAME = "Libervia" + APP_COMPONENT = "backend" + APP_NAME_ALT = "Libervia" + APP_NAME_FILE = "libervia" + APP_NAME_FULL = f"{APP_NAME} ({APP_COMPONENT})" + APP_VERSION = ( + backend.__version__ + ) # Please add 'D' at the end of version in sat/VERSION for dev versions + APP_RELEASE_NAME = "La Ruche" + APP_URL = "https://libervia.org" + + ## Runtime ## + PLUGIN_EXT = "py" + HISTORY_SKIP = "skip" + + ## Main config ## + DEFAULT_BRIDGE = "dbus" + + ## Protocol ## + XMPP_C2S_PORT = 5222 + XMPP_MAX_RETRIES = None + # default port used on Prosody, may differ on other servers + XMPP_COMPONENT_PORT = 5347 + + ## Parameters ## + NO_SECURITY_LIMIT = -1 #  FIXME: to rename + SECURITY_LIMIT_MAX = 0 + INDIVIDUAL = "individual" + GENERAL = "general" + # General parameters + HISTORY_LIMIT = "History" + SHOW_OFFLINE_CONTACTS = "Offline contacts" + SHOW_EMPTY_GROUPS = "Empty groups" + # Parameters related to connection + FORCE_SERVER_PARAM = "Force server" + FORCE_PORT_PARAM = "Force port" + # Parameters related to encryption + PROFILE_PASS_PATH = ("General", "Password") + MEMORY_CRYPTO_NAMESPACE = "crypto" # for the private persistent binary dict + MEMORY_CRYPTO_KEY = "personal_key" + # Parameters for static blog pages + # FIXME: blog constants should not be in core constants + STATIC_BLOG_KEY = "Blog page" + STATIC_BLOG_PARAM_TITLE = "Title" + STATIC_BLOG_PARAM_BANNER = "Banner" + STATIC_BLOG_PARAM_KEYWORDS = "Keywords" + STATIC_BLOG_PARAM_DESCRIPTION = "Description" + + ## Menus ## + MENU_GLOBAL = "GLOBAL" + MENU_ROOM = "ROOM" + MENU_SINGLE = "SINGLE" + MENU_JID_CONTEXT = "JID_CONTEXT" + MENU_ROSTER_JID_CONTEXT = "ROSTER_JID_CONTEXT" + MENU_ROSTER_GROUP_CONTEXT = "MENU_ROSTER_GROUP_CONTEXT" + MENU_ROOM_OCCUPANT_CONTEXT = "MENU_ROOM_OCCUPANT_CONTEXT" + + ## Profile and entities ## + PROF_KEY_NONE = "@NONE@" + PROF_KEY_DEFAULT = "@DEFAULT@" + PROF_KEY_ALL = "@ALL@" + ENTITY_ALL = "@ALL@" + ENTITY_ALL_RESOURCES = "@ALL_RESOURCES@" + ENTITY_MAIN_RESOURCE = "@MAIN_RESOURCE@" + ENTITY_CAP_HASH = "CAP_HASH" + ENTITY_TYPE = "type" + ENTITY_TYPE_MUC = "MUC" + + ## Roster jids selection ## + PUBLIC = "PUBLIC" + ALL = ( + "ALL" + ) # ALL means all known contacts, while PUBLIC means everybody, known or not + GROUP = "GROUP" + JID = "JID" + + ## Messages ## + MESS_TYPE_INFO = "info" + MESS_TYPE_CHAT = "chat" + MESS_TYPE_ERROR = "error" + MESS_TYPE_GROUPCHAT = "groupchat" + MESS_TYPE_HEADLINE = "headline" + MESS_TYPE_NORMAL = "normal" + MESS_TYPE_AUTO = "auto" # magic value to let the backend guess the type + MESS_TYPE_STANDARD = ( + MESS_TYPE_CHAT, + MESS_TYPE_ERROR, + MESS_TYPE_GROUPCHAT, + MESS_TYPE_HEADLINE, + MESS_TYPE_NORMAL, + ) + MESS_TYPE_ALL = MESS_TYPE_STANDARD + (MESS_TYPE_INFO, MESS_TYPE_AUTO) + + MESS_EXTRA_INFO = "info_type" + EXTRA_INFO_DECR_ERR = "DECRYPTION_ERROR" + EXTRA_INFO_ENCR_ERR = "ENCRYPTION_ERROR" + + # encryption is a key for plugins + MESS_KEY_ENCRYPTION: Final = "ENCRYPTION" + # encrypted is a key for frontends + MESS_KEY_ENCRYPTED = "encrypted" + MESS_KEY_TRUSTED = "trusted" + + # File encryption algorithms + ENC_AES_GCM = "AES-GCM" + + ## Chat ## + CHAT_ONE2ONE = "one2one" + CHAT_GROUP = "group" + + ## Presence ## + PRESENCE_UNAVAILABLE = "unavailable" + PRESENCE_SHOW_AWAY = "away" + PRESENCE_SHOW_CHAT = "chat" + PRESENCE_SHOW_DND = "dnd" + PRESENCE_SHOW_XA = "xa" + PRESENCE_SHOW = "show" + PRESENCE_STATUSES = "statuses" + PRESENCE_STATUSES_DEFAULT = "default" + PRESENCE_PRIORITY = "priority" + + ## Common namespaces ## + NS_XML = "http://www.w3.org/XML/1998/namespace" + NS_CLIENT = "jabber:client" + NS_COMPONENT = "jabber:component:accept" + NS_STREAM = (NS_CLIENT, NS_COMPONENT) + NS_FORWARD = "urn:xmpp:forward:0" + NS_DELAY = "urn:xmpp:delay" + NS_XHTML = "http://www.w3.org/1999/xhtml" + + ## Common XPath ## + + IQ_GET = '/iq[@type="get"]' + IQ_SET = '/iq[@type="set"]' + + ## Directories ## + + # directory for components specific data + COMPONENTS_DIR = "components" + CACHE_DIR = "cache" + # files in file dir are stored for long term + # files dir is global, i.e. for all profiles + FILES_DIR = "files" + # FILES_LINKS_DIR is a directory where files owned by a specific profile + # are linked to the global files directory. This way the directory can be + #  shared per profiles while keeping global directory where identical files + # shared between different profiles are not duplicated. + FILES_LINKS_DIR = "files_links" + # FILES_TMP_DIR is where profile's partially transfered files are put. + # Once transfer is completed, they are moved to FILES_DIR + FILES_TMP_DIR = "files_tmp" + + ## Templates ## + TEMPLATE_TPL_DIR = "templates" + TEMPLATE_THEME_DEFAULT = "default" + TEMPLATE_STATIC_DIR = "static" + # templates i18n + KEY_LANG = "lang" + KEY_THEME = "theme" + + ## Plugins ## + + # PLUGIN_INFO keys + # XXX: we use PI instead of PLUG_INFO which would normally be used + # to make the header more readable + PI_NAME = "name" + PI_IMPORT_NAME = "import_name" + PI_MAIN = "main" + PI_HANDLER = "handler" + PI_TYPE = ( + "type" + ) #  FIXME: should be types, and should handle single unicode type or tuple of types (e.g. "blog" and "import") + PI_MODES = "modes" + PI_PROTOCOLS = "protocols" + PI_DEPENDENCIES = "dependencies" + PI_RECOMMENDATIONS = "recommendations" + PI_DESCRIPTION = "description" + PI_USAGE = "usage" + + # Types + PLUG_TYPE_XEP = "XEP" + PLUG_TYPE_MISC = "MISC" + PLUG_TYPE_EXP = "EXP" + PLUG_TYPE_SEC = "SEC" + PLUG_TYPE_SYNTAXE = "SYNTAXE" + PLUG_TYPE_PUBSUB = "PUBSUB" + PLUG_TYPE_BLOG = "BLOG" + PLUG_TYPE_IMPORT = "IMPORT" + PLUG_TYPE_ENTRY_POINT = "ENTRY_POINT" + + # Modes + PLUG_MODE_CLIENT = "client" + PLUG_MODE_COMPONENT = "component" + PLUG_MODE_DEFAULT = (PLUG_MODE_CLIENT,) + PLUG_MODE_BOTH = (PLUG_MODE_CLIENT, PLUG_MODE_COMPONENT) + + # names of widely used plugins + TEXT_CMDS = "TEXT-COMMANDS" + + # PubSub event categories + PS_PEP = "PEP" + PS_MICROBLOG = "MICROBLOG" + + # PubSub + PS_PUBLISH = "publish" + PS_RETRACT = "retract" # used for items + PS_DELETE = "delete" # used for nodes + PS_PURGE = "purge" # used for nodes + PS_ITEM = "item" + PS_ITEMS = "items" # Can contain publish and retract items + PS_EVENTS = (PS_ITEMS, PS_DELETE, PS_PURGE) + + ## MESSAGE/NOTIFICATION LEVELS ## + + LVL_INFO = "info" + LVL_WARNING = "warning" + LVL_ERROR = "error" + + ## XMLUI ## + XMLUI_WINDOW = "window" + XMLUI_POPUP = "popup" + XMLUI_FORM = "form" + XMLUI_PARAM = "param" + XMLUI_DIALOG = "dialog" + XMLUI_DIALOG_CONFIRM = "confirm" + XMLUI_DIALOG_MESSAGE = "message" + XMLUI_DIALOG_NOTE = "note" + XMLUI_DIALOG_FILE = "file" + XMLUI_DATA_ANSWER = "answer" + XMLUI_DATA_CANCELLED = "cancelled" + XMLUI_DATA_TYPE = "type" + XMLUI_DATA_MESS = "message" + XMLUI_DATA_LVL = "level" + XMLUI_DATA_LVL_INFO = LVL_INFO + XMLUI_DATA_LVL_WARNING = LVL_WARNING + XMLUI_DATA_LVL_ERROR = LVL_ERROR + XMLUI_DATA_LVL_DEFAULT = XMLUI_DATA_LVL_INFO + XMLUI_DATA_LVLS = (XMLUI_DATA_LVL_INFO, XMLUI_DATA_LVL_WARNING, XMLUI_DATA_LVL_ERROR) + XMLUI_DATA_BTNS_SET = "buttons_set" + XMLUI_DATA_BTNS_SET_OKCANCEL = "ok/cancel" + XMLUI_DATA_BTNS_SET_YESNO = "yes/no" + XMLUI_DATA_BTNS_SET_DEFAULT = XMLUI_DATA_BTNS_SET_OKCANCEL + XMLUI_DATA_FILETYPE = "filetype" + XMLUI_DATA_FILETYPE_FILE = "file" + XMLUI_DATA_FILETYPE_DIR = "dir" + XMLUI_DATA_FILETYPE_DEFAULT = XMLUI_DATA_FILETYPE_FILE + + ## Logging ## + LOG_LVL_DEBUG = "DEBUG" + LOG_LVL_INFO = "INFO" + LOG_LVL_WARNING = "WARNING" + LOG_LVL_ERROR = "ERROR" + LOG_LVL_CRITICAL = "CRITICAL" + LOG_LEVELS = ( + LOG_LVL_DEBUG, + LOG_LVL_INFO, + LOG_LVL_WARNING, + LOG_LVL_ERROR, + LOG_LVL_CRITICAL, + ) + LOG_BACKEND_STANDARD = "standard" + LOG_BACKEND_TWISTED = "twisted" + LOG_BACKEND_BASIC = "basic" + LOG_BACKEND_CUSTOM = "custom" + LOG_BASE_LOGGER = "root" + LOG_TWISTED_LOGGER = "twisted" + LOG_OPT_SECTION = "DEFAULT" # section of sat.conf where log options should be + LOG_OPT_PREFIX = "log_" + # (option_name, default_value) tuples + LOG_OPT_COLORS = ( + "colors", + "true", + ) # true for auto colors, force to have colors even if stdout is not a tty, false for no color + LOG_OPT_TAINTS_DICT = ( + "levels_taints_dict", + { + LOG_LVL_DEBUG: ("cyan",), + LOG_LVL_INFO: (), + LOG_LVL_WARNING: ("yellow",), + LOG_LVL_ERROR: ("red", "blink", r"/!\ ", "blink_off"), + LOG_LVL_CRITICAL: ("bold", "red", "Guru Meditation ", "normal_weight"), + }, + ) + LOG_OPT_LEVEL = ("level", "info") + LOG_OPT_FORMAT = ("fmt", "%(message)s") # similar to logging format. + LOG_OPT_LOGGER = ("logger", "") # regex to filter logger name + LOG_OPT_OUTPUT_SEP = "//" + LOG_OPT_OUTPUT_DEFAULT = "default" + LOG_OPT_OUTPUT_MEMORY = "memory" + LOG_OPT_OUTPUT_MEMORY_LIMIT = 300 + LOG_OPT_OUTPUT_FILE = "file" # file is implicit if only output + LOG_OPT_OUTPUT = ( + "output", + LOG_OPT_OUTPUT_SEP + LOG_OPT_OUTPUT_DEFAULT, + ) # //default = normal output (stderr or a file with twistd), path/to/file for a file (must be the first if used), //memory for memory (options can be put in parenthesis, e.g.: //memory(500) for a 500 lines memory) + + ## action constants ## + META_TYPE_FILE = "file" + META_TYPE_CALL = "call" + META_TYPE_OVERWRITE = "overwrite" + META_TYPE_NOT_IN_ROSTER_LEAK = "not_in_roster_leak" + META_SUBTYPE_CALL_AUDIO = "audio" + META_SUBTYPE_CALL_VIDEO = "video" + + ## HARD-CODED ACTIONS IDS (generated with uuid.uuid4) ## + AUTHENTICATE_PROFILE_ID = "b03bbfa8-a4ae-4734-a248-06ce6c7cf562" + CHANGE_XMPP_PASSWD_ID = "878b9387-de2b-413b-950f-e424a147bcd0" + + ## Text values ## + BOOL_TRUE = "true" + BOOL_FALSE = "false" + + ## Special values used in bridge methods calls ## + HISTORY_LIMIT_DEFAULT = -1 + HISTORY_LIMIT_NONE = -2 + + ## Progress error special values ## + PROGRESS_ERROR_DECLINED = "declined" #  session has been declined by peer user + PROGRESS_ERROR_FAILED = "failed" #  something went wrong with the session + + ## Files ## + FILE_TYPE_DIRECTORY = "directory" + FILE_TYPE_FILE = "file" + # when filename can't be found automatically, this one will be used + FILE_DEFAULT_NAME = "unnamed" + + ## Permissions management ## + ACCESS_PERM_READ = "read" + ACCESS_PERM_WRITE = "write" + ACCESS_PERMS = {ACCESS_PERM_READ, ACCESS_PERM_WRITE} + ACCESS_TYPE_PUBLIC = "public" + ACCESS_TYPE_WHITELIST = "whitelist" + ACCESS_TYPES = (ACCESS_TYPE_PUBLIC, ACCESS_TYPE_WHITELIST) + + ## Common data keys ## + KEY_THUMBNAILS = "thumbnails" + KEY_PROGRESS_ID = "progress_id" + KEY_ATTACHMENTS = "attachments" + KEY_ATTACHMENTS_MEDIA_TYPE = "media_type" + KEY_ATTACHMENTS_PREVIEW = "preview" + KEY_ATTACHMENTS_RESIZE = "resize" + + + ## Common extra keys/values ## + KEY_ORDER_BY = "order_by" + KEY_USE_CACHE = "use_cache" + KEY_DECRYPT = "decrypt" + + ORDER_BY_CREATION = 'creation' + ORDER_BY_MODIFICATION = 'modification' + + # internationalisation + DEFAULT_LOCALE = "en_GB" + + ## Command Line ## + + # Exit codes used by CLI applications + EXIT_OK = 0 + EXIT_ERROR = 1 # generic error, when nothing else match + EXIT_BAD_ARG = 2 # arguments given by user are bad + EXIT_BRIDGE_ERROR = 3 # can't connect to bridge + EXIT_BRIDGE_ERRBACK = 4 # something went wrong when calling a bridge method + EXIT_BACKEND_NOT_FOUND = 5 # can't find backend with this bride + EXIT_NOT_FOUND = 16 # an item required by a command was not found + EXIT_DATA_ERROR = 17 # data needed for a command is invalid + EXIT_MISSING_FEATURE = 18 # a needed plugin or feature is not available + EXIT_CONFLICT = 19 # an item already exists + EXIT_USER_CANCELLED = 20 # user cancelled action + EXIT_INTERNAL_ERROR = 111 # unexpected error + EXIT_FILE_NOT_EXE = ( + 126 + ) # a file to be executed was found, but it was not an executable utility (cf. man 1 exit) + EXIT_CMD_NOT_FOUND = 127 # a utility to be executed was not found (cf. man 1 exit) + EXIT_CMD_ERROR = 127 # a utility to be executed returned an error exit code + EXIT_SIGNAL_INT = 128 # a command was interrupted by a signal (cf. man 1 exit) + + ## Misc ## + SAVEFILE_DATABASE = APP_NAME_FILE + ".db" + IQ_SET = '/iq[@type="set"]' + ENV_PREFIX = "SAT_" # Prefix used for environment variables + IGNORE = "ignore" + NO_LIMIT = -1 # used in bridge when a integer value is expected + DEFAULT_MAX_AGE = 1209600 # default max age of cached files, in seconds + STANZA_NAMES = ("iq", "message", "presence") + + # Stream Hooks + STREAM_HOOK_SEND = "send" + STREAM_HOOK_RECEIVE = "receive" + + @classmethod + def LOG_OPTIONS(cls): + """Return options checked for logs""" + # XXX: we use a classmethod so we can use Const inheritance to change default options + return ( + cls.LOG_OPT_COLORS, + cls.LOG_OPT_TAINTS_DICT, + cls.LOG_OPT_LEVEL, + cls.LOG_OPT_FORMAT, + cls.LOG_OPT_LOGGER, + cls.LOG_OPT_OUTPUT, + ) + + @classmethod + def bool(cls, value: str) -> bool: + """@return (bool): bool value for associated constant""" + assert isinstance(value, str) + return value.lower() in (cls.BOOL_TRUE, "1", "yes", "on") + + @classmethod + def bool_const(cls, value: bool) -> str: + """@return (str): constant associated to bool value""" + assert isinstance(value, bool) + return cls.BOOL_TRUE if value else cls.BOOL_FALSE + + + +## Configuration ## +if ( + BaseDirectory +): # skipped when xdg module is not available (should not happen in backend) + if "org.libervia.cagou" in BaseDirectory.__file__: + # FIXME: hack to make config read from the right location on Android + # TODO: fix it in a more proper way + + # we need to use Android API to get downloads directory + import os.path + from jnius import autoclass + + # we don't want the very verbose jnius log when we are in DEBUG level + import logging + logging.getLogger('jnius').setLevel(logging.WARNING) + logging.getLogger('jnius.reflect').setLevel(logging.WARNING) + + Environment = autoclass("android.os.Environment") + + BaseDirectory = None + Const.DEFAULT_CONFIG = { + "local_dir": "/data/data/org.libervia.cagou/app", + "media_dir": "/data/data/org.libervia.cagou/files/app/media", + # FIXME: temporary location for downloads, need to call API properly + "downloads_dir": os.path.join( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + ).getAbsolutePath(), + Const.APP_NAME_FILE, + ), + "pid_dir": "%(local_dir)s", + "log_dir": "%(local_dir)s", + } + Const.CONFIG_FILES = [ + "/data/data/org.libervia.cagou/files/app/android/" + + Const.APP_NAME_FILE + + ".conf" + ] + else: + import os + # we use parent of "sat" module dir as last config path, this is useful for + # per instance configurations (e.g. a dev instance and a main instance) + root_dir = dirname(dirname(backend.__file__)) + '/' + Const.CONFIG_PATHS = ( + # /etc/_sat.conf is used for system-related settings (e.g. when media_dir + # is set by the distribution and has not reason to change, or in a Docker + # image) + ["/etc/_", "/etc/", "~/", "~/."] + + [ + "{}/".format(path) + for path in list(BaseDirectory.load_config_paths(Const.APP_NAME_FILE)) + ] + # this is to handle legacy sat.conf + + [ + "{}/".format(path) + for path in list(BaseDirectory.load_config_paths("sat")) + ] + + [root_dir] + ) + + # on recent versions of Flatpak, FLATPAK_ID is set at run time + # it seems that this is not the case on older versions, + # but FLATPAK_SANDBOX_DIR seems set then + if os.getenv('FLATPAK_ID') or os.getenv('FLATPAK_SANDBOX_DIR'): + # for Flatpak, the conf can't be set in /etc or $HOME, so we have + # to add /app + Const.CONFIG_PATHS.append('/app/') + + ## Configuration ## + Const.DEFAULT_CONFIG = { + "media_dir": "/usr/share/" + Const.APP_NAME_FILE + "/media", + "local_dir": BaseDirectory.save_data_path(Const.APP_NAME_FILE), + "downloads_dir": "~/Downloads/" + Const.APP_NAME_FILE, + "pid_dir": "%(local_dir)s", + "log_dir": "%(local_dir)s", + } + + # List of the configuration filenames sorted by ascending priority + Const.CONFIG_FILES = [ + realpath(expanduser(path) + Const.APP_NAME_FILE + ".conf") + for path in Const.CONFIG_PATHS + ] + [ + # legacy sat.conf + realpath(expanduser(path) + "sat.conf") + for path in Const.CONFIG_PATHS + ] + diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/core_types.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/core_types.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +# Libervia types +# Copyright (C) 2011 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from collections import namedtuple +from typing import Dict, Callable, Optional +from typing_extensions import TypedDict + +from twisted.words.protocols.jabber import jid as t_jid +from twisted.words.protocols.jabber import xmlstream +from twisted.words.xish import domish + + +class SatXMPPEntity: + + profile: str + jid: t_jid.JID + is_component: bool + server_jid: t_jid.JID + IQ: Callable[[Optional[str], Optional[int]], xmlstream.IQ] + +EncryptionPlugin = namedtuple("EncryptionPlugin", ("instance", + "name", + "namespace", + "priority", + "directed")) + + +class EncryptionSession(TypedDict): + plugin: EncryptionPlugin + + +# Incomplete types built through observation rather than code inspection. +MessageDataExtra = TypedDict( + "MessageDataExtra", + { "encrypted": bool, "origin_id": str }, + total=False +) + + +MessageData = TypedDict("MessageData", { + "from": t_jid.JID, + "to": t_jid.JID, + "uid": str, + "message": Dict[str, str], + "subject": Dict[str, str], + "type": str, + "timestamp": float, + "extra": MessageDataExtra, + "ENCRYPTION": EncryptionSession, + "xml": domish.Element +}, total=False) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/exceptions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/exceptions.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + + +# SàT Exceptions +# Copyright (C) 2011 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +class ProfileUnknownError(Exception): + pass + + +class ProfileNotInCacheError(Exception): + pass + + +class ProfileNotSetError(Exception): + """This error raises when no profile has been set (value @NONE@ is found, but it should have been replaced)""" + + +class ProfileConnected(Exception): + """This error is raised when trying to delete a connected profile.""" + + +class ProfileNotConnected(Exception): + pass + + +class ProfileKeyUnknown(Exception): + pass + + +class ClientTypeError(Exception): + """This code is not allowed for this type of client (i.e. component or not)""" + + +class UnknownEntityError(Exception): + pass + + +class UnknownGroupError(Exception): + pass + + +class MissingModule(Exception): + # Used to indicate when a plugin dependence is not found + # it's nice to indicate when to find the dependence in argument string + pass + + +class MissingPlugin(Exception): + """A SàT plugin needed for a feature/method is missing""" + pass + + +class NotFound(Exception): + pass + + +class ConfigError(Exception): + pass + + +class DataError(Exception): + pass + + +class ExternalRequestError(Exception): + """Request to third party server failed""" + + +class ConflictError(Exception): + pass + + +class TimeOutError(Exception): + pass + + +class CancelError(Exception): + pass + + +class InternalError(Exception): + pass + + +class FeatureNotFound( + Exception +): # a disco feature/identity which is needed is not present + pass + + +class BridgeInitError(Exception): + pass + + +class BridgeExceptionNoService(Exception): + pass + + +class DatabaseError(Exception): + pass + + +class PasswordError(Exception): + pass + + +class PermissionError(Exception): + pass + + +class ParsingError(ValueError): + pass + + +class EncryptionError(Exception): + """Invalid encryption""" + pass + + +# Something which need to be done is not available yet +class NotReady(Exception): + pass + + +class NetworkError(Exception): + """Something is wrong with a request (e.g. HTTP(S))""" + + +class InvalidCertificate(Exception): + """A TLS certificate is not valid""" + pass + + +class CommandException(RuntimeError): + """An external command failed + + stdout and stderr will be attached to the Exception + """ + + def __init__(self, msg, stdout, stderr): + super(CommandException, self).__init__(msg) + self.stdout = stdout + self.stderr = stderr diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/i18n.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Callable, cast + +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) + +try: + + import gettext + + _ = gettext.translation("libervia_backend", "i18n", fallback=True).gettext + _translators = {None: gettext.NullTranslations()} + + def language_switch(lang=None): + if not lang in _translators: + _translators[lang] = gettext.translation( + "libervia_backendt", languages=[lang], fallback=True + ) + _translators[lang].install() + + +except ImportError: + + log.warning("gettext support disabled") + _ = cast(Callable[[str], str], lambda msg: msg) # Libervia doesn't support gettext + + def language_switch(lang=None): + pass + + +D_ = cast(Callable[[str], str], lambda msg: msg) # used for deferred translations diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/launcher.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/launcher.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Script launching SàT backend""" + +import sys +import os +import argparse +from pathlib import Path +from configparser import ConfigParser +from twisted.application import app +from twisted.python import usage +from libervia.backend.core.constants import Const as C + + +class LiberviaLogger(app.AppLogger): + + def start(self, application): + # logging is initialised by sat.core.log_config via the Twisted plugin, nothing + # to do here + self._initialLog() + + def stop(self): + pass + + +class Launcher: + APP_NAME=C.APP_NAME + APP_NAME_FILE=C.APP_NAME_FILE + + @property + def NOT_RUNNING_MSG(self): + return f"{self.APP_NAME} is *NOT* running" + + def cmd_no_subparser(self, args): + """Command launched by default""" + args.extra_args = [] + self.cmd_background(args) + + def cmd_background(self, args): + self.run_twistd(args) + + def cmd_foreground(self, args): + self.run_twistd(args, twistd_opts=['--nodaemon']) + + def cmd_debug(self, args): + self.run_twistd(args, twistd_opts=['--debug']) + + def cmd_stop(self, args): + import signal + import time + config = self.get_config() + pid_file = self.get_pid_file(config) + if not pid_file.is_file(): + print(self.NOT_RUNNING_MSG) + sys.exit(0) + try: + pid = int(pid_file.read_text()) + except Exception as e: + print(f"Can't read PID file at {pid_file}: {e}") + # we use the same exit code as DATA_ERROR in jp + sys.exit(17) + print(f"Terminating {self.APP_NAME}…") + os.kill(pid, signal.SIGTERM) + kill_started = time.time() + state = "init" + import errno + while True: + try: + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + break + elif e.errno == errno.EPERM: + print(f"Can't kill {self.APP_NAME}, the process is owned by an other user", file=sys.stderr) + sys.exit(18) + else: + raise e + time.sleep(0.2) + now = time.time() + if state == 'init' and now - kill_started > 5: + if state == 'init': + state = 'waiting' + print(f"Still waiting for {self.APP_NAME} to be terminated…") + elif state == 'waiting' and now - kill_started > 10: + state == 'killing' + print("Waiting for too long, we kill the process") + os.kill(pid, signal.SIGKILL) + sys.exit(1) + + sys.exit(0) + + def cmd_status(self, args): + config = self.get_config() + pid_file = self.get_pid_file(config) + if pid_file.is_file(): + import errno + try: + pid = int(pid_file.read_text()) + except Exception as e: + print(f"Can't read PID file at {pid_file}: {e}") + # we use the same exit code as DATA_ERROR in jp + sys.exit(17) + # we check if there is a process + # inspired by https://stackoverflow.com/a/568285 and https://stackoverflow.com/a/6940314 + try: + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + running = False + elif e.errno == errno.EPERM: + print("Process {pid} is run by an other user") + running = True + else: + running = True + + if running: + print(f"{self.APP_NAME} is running (pid: {pid})") + sys.exit(0) + else: + print(f"{self.NOT_RUNNING_MSG}, but a pid file is present (bad exit ?): {pid_file}") + sys.exit(2) + else: + print(self.NOT_RUNNING_MSG) + sys.exit(1) + + def parse_args(self): + parser = argparse.ArgumentParser(description=f"Launch {self.APP_NAME} backend") + parser.set_defaults(cmd=self.cmd_no_subparser) + subparsers = parser.add_subparsers() + extra_help = f"arguments to pass to {self.APP_NAME} service" + + bg_parser = subparsers.add_parser( + 'background', + aliases=['bg'], + help=f"run {self.APP_NAME} backend in background (as a daemon)") + bg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) + bg_parser.set_defaults(cmd=self.cmd_background) + + fg_parser = subparsers.add_parser( + 'foreground', + aliases=['fg'], + help=f"run {self.APP_NAME} backend in foreground") + fg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) + fg_parser.set_defaults(cmd=self.cmd_foreground) + + dbg_parser = subparsers.add_parser( + 'debug', + aliases=['dbg'], + help=f"run {self.APP_NAME} backend in debug mode") + dbg_parser.add_argument('extra_args', nargs=argparse.REMAINDER, help=extra_help) + dbg_parser.set_defaults(cmd=self.cmd_debug) + + stop_parser = subparsers.add_parser( + 'stop', + help=f"stop running {self.APP_NAME} backend") + stop_parser.set_defaults(cmd=self.cmd_stop) + + status_parser = subparsers.add_parser( + 'status', + help=f"indicate if {self.APP_NAME} backend is running") + status_parser.set_defaults(cmd=self.cmd_status) + + return parser.parse_args() + + def get_config(self): + config = ConfigParser(defaults=C.DEFAULT_CONFIG) + try: + config.read(C.CONFIG_FILES) + except Exception as e: + print (rf"/!\ Can't read main config! {e}") + sys.exit(1) + return config + + def get_pid_file(self, config): + pid_dir = Path(config.get('DEFAULT', 'pid_dir')).expanduser() + return pid_dir / f"{self.APP_NAME_FILE}.pid" + + def run_twistd(self, args, twistd_opts=None): + """Run twistd settings options with args""" + from twisted.python.runtime import platformType + if platformType == "win32": + from twisted.scripts._twistw import (ServerOptions, + WindowsApplicationRunner as app_runner) + else: + from twisted.scripts._twistd_unix import (ServerOptions, + UnixApplicationRunner as app_runner) + + app_runner.loggerFactory = LiberviaLogger + server_options = ServerOptions() + config = self.get_config() + pid_file = self.get_pid_file(config) + log_dir = Path(config.get('DEFAULT', 'log_dir')).expanduser() + log_file = log_dir / f"{self.APP_NAME_FILE}.log" + server_opts = [ + '--no_save', + '--pidfile', str(pid_file), + '--logfile', str(log_file), + ] + if twistd_opts is not None: + server_opts.extend(twistd_opts) + server_opts.append(self.APP_NAME_FILE) + if args.extra_args: + try: + args.extra_args.remove('--') + except ValueError: + pass + server_opts.extend(args.extra_args) + try: + server_options.parseOptions(server_opts) + except usage.error as ue: + print(server_options) + print("%s: %s" % (sys.argv[0], ue)) + sys.exit(1) + else: + runner = app_runner(server_options) + runner.run() + if runner._exitSignal is not None: + app._exitWithSignal(runner._exitSignal) + try: + sys.exit(app._exitCode) + except AttributeError: + pass + + @classmethod + def run(cls): + args = cls().parse_args() + args.cmd(args) + + +if __name__ == '__main__': + Launcher.run() diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/log.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/log.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,426 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""High level logging functions""" +# XXX: this module use standard logging module when possible, but as SàT can work in different cases where logging is not the best choice (twisted, pyjamas, etc), it is necessary to have a dedicated module. Additional feature like environment variables and colors are also managed. +# TODO: change formatting from "%s" style to "{}" when moved to Python 3 + +from typing import TYPE_CHECKING, Any, Optional, Dict + +if TYPE_CHECKING: + from logging import _ExcInfoType +else: + _ExcInfoType = Any + +from libervia.backend.core.constants import Const as C +from libervia.backend.tools.common.ansi import ANSI as A +from libervia.backend.core import exceptions +import traceback + +backend = None +_loggers: Dict[str, "Logger"] = {} +handlers = {} +COLOR_START = '%(color_start)s' +COLOR_END = '%(color_end)s' + + +class Filtered(Exception): + pass + + +class Logger: + """High level logging class""" + fmt = None # format option as given by user (e.g. SAT_LOG_LOGGER) + filter_name = None # filter to call + post_treat = None + + def __init__(self, name): + if isinstance(name, Logger): + self.copy(name) + else: + self._name = name + + def copy(self, other): + """Copy values from other Logger""" + self.fmt = other.fmt + self.Filter_name = other.fmt + self.post_treat = other.post_treat + self._name = other._name + + def add_traceback(self, message): + tb = traceback.format_exc() + return message + "\n==== traceback ====\n" + tb + + def out( + self, + message: object, + level: Optional[str] = None, + exc_info: _ExcInfoType = False, + **kwargs + ) -> None: + """Actually log the message + + @param message: formatted message + """ + if exc_info: + message = self.add_traceback(message) + print(message) + + def log( + self, + level: str, + message: object, + exc_info: _ExcInfoType = False, + **kwargs + ) -> None: + """Print message + + @param level: one of C.LOG_LEVELS + @param message: message to format and print + """ + if exc_info: + message = self.add_traceback(message) + try: + formatted = self.format(level, message) + if self.post_treat is None: + self.out(formatted, level, **kwargs) + else: + self.out(self.post_treat(level, formatted), level, **kwargs) + except Filtered: + pass + + def format(self, level: str, message: object) -> object: + """Format message according to Logger.fmt + + @param level: one of C.LOG_LEVELS + @param message: message to format + @return: formatted message + + @raise: Filtered when the message must not be logged + """ + if self.fmt is None and self.filter_name is None: + return message + record = {'name': self._name, + 'message': message, + 'levelname': level, + } + try: + if not self.filter_name.dict_filter(record): + raise Filtered + except (AttributeError, TypeError): # XXX: TypeError is here because of a pyjamas bug which need to be fixed (TypeError is raised instead of AttributeError) + if self.filter_name is not None: + raise ValueError("Bad filter: filters must have a .filter method") + try: + return self.fmt % record + except TypeError: + return message + except KeyError as e: + if e.args[0] == 'profile': + # XXX: %(profile)s use some magic with introspection, for debugging purpose only *DO NOT* use in production + record['profile'] = configure_cls[backend].get_profile() + return self.fmt % record + else: + raise e + + def debug(self, msg: object, **kwargs) -> None: + self.log(C.LOG_LVL_DEBUG, msg, **kwargs) + + def info(self, msg: object, **kwargs) -> None: + self.log(C.LOG_LVL_INFO, msg, **kwargs) + + def warning(self, msg: object, **kwargs) -> None: + self.log(C.LOG_LVL_WARNING, msg, **kwargs) + + def error(self, msg: object, **kwargs) -> None: + self.log(C.LOG_LVL_ERROR, msg, **kwargs) + + def critical(self, msg: object, **kwargs) -> None: + self.log(C.LOG_LVL_CRITICAL, msg, **kwargs) + + def exception(self, msg: object, exc_info=True, **kwargs) -> None: + self.log(C.LOG_LVL_ERROR, msg, exc_info=exc_info, **kwargs) + + +class FilterName(object): + """Filter on logger name according to a regex""" + + def __init__(self, name_re): + """Initialise name filter + + @param name_re: regular expression used to filter names (using search and not match) + """ + assert name_re + import re + self.name_re = re.compile(name_re) + + def filter(self, record): + if self.name_re.search(record.name) is not None: + return 1 + return 0 + + def dict_filter(self, dict_record): + """Filter using a dictionary record + + @param dict_record: dictionary with at list a key "name" with logger name + @return: True if message should be logged + """ + class LogRecord(object): + pass + log_record = LogRecord() + log_record.name = dict_record['name'] + return self.filter(log_record) == 1 + + +class ConfigureBase: + LOGGER_CLASS = Logger + # True if color location is specified in fmt (with COLOR_START) + _color_location = False + + def __init__(self, level=None, fmt=None, output=None, logger=None, colors=False, + levels_taints_dict=None, force_colors=False, backend_data=None): + """Configure a backend + + @param level: one of C.LOG_LEVELS + @param fmt: format string, pretty much as in std logging. + Accept the following keywords (maybe more depending on backend): + - "message" + - "levelname" + - "name" (logger name) + @param logger: if set, use it as a regular expression to filter on logger name. + Use search to match expression, so ^ or $ can be necessary. + @param colors: if True use ANSI colors to show log levels + @param force_colors: if True ANSI colors are used even if stdout is not a tty + """ + self.backend_data = backend_data + self.pre_treatment() + self.configure_level(level) + self.configure_format(fmt) + self.configure_output(output) + self.configure_logger(logger) + self.configure_colors(colors, force_colors, levels_taints_dict) + self.post_treatment() + self.update_current_logger() + + def update_current_logger(self): + """update existing logger to the class needed for this backend""" + if self.LOGGER_CLASS is None: + return + for name, logger in list(_loggers.items()): + _loggers[name] = self.LOGGER_CLASS(logger) + + def pre_treatment(self): + pass + + def configure_level(self, level): + if level is not None: + # we deactivate methods below level + level_idx = C.LOG_LEVELS.index(level) + def dev_null(self, msg): + pass + for _level in C.LOG_LEVELS[:level_idx]: + setattr(Logger, _level.lower(), dev_null) + + def configure_format(self, fmt): + if fmt is not None: + if fmt != '%(message)s': # %(message)s is the same as None + Logger.fmt = fmt + if COLOR_START in fmt: + ConfigureBase._color_location = True + if fmt.find(COLOR_END,fmt.rfind(COLOR_START))<0: + # color_start not followed by an end, we add it + Logger.fmt += COLOR_END + + def configure_output(self, output): + if output is not None: + if output != C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT: + # TODO: manage other outputs + raise NotImplementedError("Basic backend only manage default output yet") + + def configure_logger(self, logger): + if logger: + Logger.filter_name = FilterName(logger) + + def configure_colors(self, colors, force_colors, levels_taints_dict): + if colors: + # if color are used, we need to handle levels_taints_dict + for level in list(levels_taints_dict.keys()): + # we wants levels in uppercase to correspond to contstants + levels_taints_dict[level.upper()] = levels_taints_dict[level] + taints = self.__class__.taints = {} + for level in C.LOG_LEVELS: + # we want use values and use constant value as default + taint_list = levels_taints_dict.get(level, C.LOG_OPT_TAINTS_DICT[1][level]) + ansi_list = [] + for elt in taint_list: + elt = elt.upper() + try: + ansi = getattr(A, 'FG_{}'.format(elt)) + except AttributeError: + try: + ansi = getattr(A, elt) + except AttributeError: + # we use raw string if element is unknown + ansi = elt + ansi_list.append(ansi) + taints[level] = ''.join(ansi_list) + + def post_treatment(self): + pass + + def manage_outputs(self, outputs_raw): + """ Parse output option in a backend agnostic way, and fill handlers consequently + + @param outputs_raw: output option as enterred in environment variable or in configuration + """ + if not outputs_raw: + return + outputs = outputs_raw.split(C.LOG_OPT_OUTPUT_SEP) + global handlers + if len(outputs) == 1: + handlers[C.LOG_OPT_OUTPUT_FILE] = [outputs.pop()] + + for output in outputs: + if not output: + continue + if output[-1] == ')': + # we have options + opt_begin = output.rfind('(') + options = output[opt_begin+1:-1] + output = output[:opt_begin] + else: + options = None + + if output not in (C.LOG_OPT_OUTPUT_DEFAULT, C.LOG_OPT_OUTPUT_FILE, C.LOG_OPT_OUTPUT_MEMORY): + raise ValueError("Invalid output [%s]" % output) + + if output == C.LOG_OPT_OUTPUT_DEFAULT: + # no option for defaut handler + handlers[output] = None + elif output == C.LOG_OPT_OUTPUT_FILE: + if not options: + ValueError("{handler} output need a path as option" .format(handle=output)) + handlers.setdefault(output, []).append(options) + options = None # option are parsed, we can empty them + elif output == C.LOG_OPT_OUTPUT_MEMORY: + # we have memory handler, option can be the len limit or None + try: + limit = int(options) + options = None # option are parsed, we can empty them + except (TypeError, ValueError): + limit = C.LOG_OPT_OUTPUT_MEMORY_LIMIT + handlers[output] = limit + + if options: # we should not have unparsed options + raise ValueError("options [{options}] are not supported for {handler} output".format(options=options, handler=output)) + + @staticmethod + def memory_get(size=None): + """Return buffered logs + + @param size: number of logs to return + """ + raise NotImplementedError + + @classmethod + def ansi_colors(cls, level, message): + """Colorise message depending on level for terminals + + @param level: one of C.LOG_LEVELS + @param message: formatted message to log + @return: message with ANSI escape codes for coloration + """ + + try: + start = cls.taints[level] + except KeyError: + start = '' + + if cls._color_location: + return message % {'color_start': start, + 'color_end': A.RESET} + else: + return '%s%s%s' % (start, message, A.RESET) + + @staticmethod + def get_profile(): + """Try to find profile value using introspection""" + raise NotImplementedError + + +class ConfigureCustom(ConfigureBase): + LOGGER_CLASS = None + + def __init__(self, logger_class, *args, **kwargs): + ConfigureCustom.LOGGER_CLASS = logger_class + + +configure_cls = { None: ConfigureBase, + C.LOG_BACKEND_CUSTOM: ConfigureCustom + } # XXX: (key: backend, value: Configure subclass) must be filled when new backend are added + + +def configure(backend_, **options): + """Configure logging behaviour + @param backend: can be: + C.LOG_BACKEND_BASIC: use a basic print based logging + C.LOG_BACKEND_CUSTOM: use a given Logger subclass + """ + global backend + if backend is not None: + raise exceptions.InternalError("Logging can only be configured once") + backend = backend_ + + try: + configure_class = configure_cls[backend] + except KeyError: + raise ValueError("unknown backend [{}]".format(backend)) + if backend == C.LOG_BACKEND_CUSTOM: + logger_class = options.pop('logger_class') + configure_class(logger_class, **options) + else: + configure_class(**options) + +def memory_get(size=None): + if not C.LOG_OPT_OUTPUT_MEMORY in handlers: + raise ValueError('memory output is not used') + return configure_cls[backend].memory_get(size) + +def getLogger(name=C.LOG_BASE_LOGGER) -> Logger: + try: + logger_class = configure_cls[backend].LOGGER_CLASS + except KeyError: + raise ValueError("This method should not be called with backend [{}]".format(backend)) + return _loggers.setdefault(name, logger_class(name)) + +_root_logger = getLogger() + +def debug(msg, **kwargs): + _root_logger.debug(msg, **kwargs) + +def info(msg, **kwargs): + _root_logger.info(msg, **kwargs) + +def warning(msg, **kwargs): + _root_logger.warning(msg, **kwargs) + +def error(msg, **kwargs): + _root_logger.error(msg, **kwargs) + +def critical(msg, **kwargs): + _root_logger.critical(msg, **kwargs) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/log_config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/log_config.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 + + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""High level logging functions""" +# XXX: this module use standard logging module when possible, but as SàT can work in different cases where logging is not the best choice (twisted, pyjamas, etc), it is necessary to have a dedicated module. Additional feature like environment variables and colors are also managed. + +from libervia.backend.core.constants import Const as C +from libervia.backend.core import log + + +class TwistedLogger(log.Logger): + colors = True + force_colors = False + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + from twisted.logger import Logger + self.twisted_log = Logger() + + def out(self, message, level=None, **kwargs): + """Actually log the message + + @param message: formatted message + """ + self.twisted_log.emit( + level=self.level_map[level], + format=message, + sat_logged=True, + **kwargs, + ) + + +class ConfigureBasic(log.ConfigureBase): + def configure_colors(self, colors, force_colors, levels_taints_dict): + super(ConfigureBasic, self).configure_colors( + colors, force_colors, levels_taints_dict + ) + if colors: + import sys + + try: + isatty = sys.stdout.isatty() + except AttributeError: + isatty = False + # FIXME: isatty should be tested on each handler, not globaly + if (force_colors or isatty): + # we need colors + log.Logger.post_treat = lambda logger, level, message: self.ansi_colors( + level, message + ) + elif force_colors: + raise ValueError("force_colors can't be used if colors is False") + + @staticmethod + def get_profile(): + """Try to find profile value using introspection""" + import inspect + + stack = inspect.stack() + current_path = stack[0][1] + for frame_data in stack[:-1]: + if frame_data[1] != current_path: + if ( + log.backend == C.LOG_BACKEND_STANDARD + and "/logging/__init__.py" in frame_data[1] + ): + continue + break + + frame = frame_data[0] + args = inspect.getargvalues(frame) + try: + profile = args.locals.get("profile") or args.locals["profile_key"] + except (TypeError, KeyError): + try: + try: + profile = args.locals["self"].profile + except AttributeError: + try: + profile = args.locals["self"].parent.profile + except AttributeError: + profile = args.locals[ + "self" + ].host.profile # used in quick_frontend for single profile configuration + except Exception: + # we can't find profile, we return an empty value + profile = "" + return profile + + +class ConfigureTwisted(ConfigureBasic): + LOGGER_CLASS = TwistedLogger + + def pre_treatment(self): + from twisted import logger + global logger + self.level_map = { + C.LOG_LVL_DEBUG: logger.LogLevel.debug, + C.LOG_LVL_INFO: logger.LogLevel.info, + C.LOG_LVL_WARNING: logger.LogLevel.warn, + C.LOG_LVL_ERROR: logger.LogLevel.error, + C.LOG_LVL_CRITICAL: logger.LogLevel.critical, + } + self.LOGGER_CLASS.level_map = self.level_map + + def configure_level(self, level): + self.level = self.level_map[level] + + def configure_output(self, output): + import sys + from twisted.python import logfile + self.log_publisher = logger.LogPublisher() + + if output is None: + output = C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT + self.manage_outputs(output) + + if C.LOG_OPT_OUTPUT_DEFAULT in log.handlers: + if self.backend_data is None: + raise ValueError( + "You must pass options as backend_data with Twisted backend" + ) + options = self.backend_data + log_file = logfile.LogFile.fromFullPath(options['logfile']) + self.log_publisher.addObserver( + logger.FileLogObserver(log_file, self.text_formatter)) + # we also want output to stdout if we are in debug or nodaemon mode + if options.get("nodaemon", False) or options.get("debug", False): + self.log_publisher.addObserver( + logger.FileLogObserver(sys.stdout, self.text_formatter)) + + if C.LOG_OPT_OUTPUT_FILE in log.handlers: + + for path in log.handlers[C.LOG_OPT_OUTPUT_FILE]: + log_file = ( + sys.stdout if path == "-" else logfile.LogFile.fromFullPath(path) + ) + self.log_publisher.addObserver( + logger.FileLogObserver(log_file, self.text_formatter)) + + if C.LOG_OPT_OUTPUT_MEMORY in log.handlers: + raise NotImplementedError( + "Memory observer is not implemented in Twisted backend" + ) + + def configure_colors(self, colors, force_colors, levels_taints_dict): + super(ConfigureTwisted, self).configure_colors( + colors, force_colors, levels_taints_dict + ) + self.LOGGER_CLASS.colors = colors + self.LOGGER_CLASS.force_colors = force_colors + if force_colors and not colors: + raise ValueError("colors must be True if force_colors is True") + + def post_treatment(self): + """Install twistedObserver which manage non SàT logs""" + # from twisted import logger + import sys + filtering_obs = logger.FilteringLogObserver( + observer=self.log_publisher, + predicates=[ + logger.LogLevelFilterPredicate(self.level), + ] + ) + logger.globalLogBeginner.beginLoggingTo([filtering_obs]) + + def text_formatter(self, event): + if event.get('sat_logged', False): + timestamp = ''.join([logger.formatTime(event.get("log_time", None)), " "]) + return f"{timestamp}{event.get('log_format', '')}\n" + else: + eventText = logger.eventAsText( + event, includeSystem=True) + if not eventText: + return None + return eventText.replace("\n", "\n\t") + "\n" + + +class ConfigureStandard(ConfigureBasic): + def __init__( + self, + level=None, + fmt=None, + output=None, + logger=None, + colors=False, + levels_taints_dict=None, + force_colors=False, + backend_data=None, + ): + if fmt is None: + fmt = C.LOG_OPT_FORMAT[1] + if output is None: + output = C.LOG_OPT_OUTPUT[1] + super(ConfigureStandard, self).__init__( + level, + fmt, + output, + logger, + colors, + levels_taints_dict, + force_colors, + backend_data, + ) + + def pre_treatment(self): + """We use logging methods directly, instead of using Logger""" + import logging + + log.getLogger = logging.getLogger + log.debug = logging.debug + log.info = logging.info + log.warning = logging.warning + log.error = logging.error + log.critical = logging.critical + + def configure_level(self, level): + if level is None: + level = C.LOG_LVL_DEBUG + self.level = level + + def configure_format(self, fmt): + super(ConfigureStandard, self).configure_format(fmt) + import logging + + class SatFormatter(logging.Formatter): + """Formatter which manage SàT specificities""" + _format = fmt + _with_profile = "%(profile)s" in fmt + + def __init__(self, can_colors=False): + super(SatFormatter, self).__init__(self._format) + self.can_colors = can_colors + + def format(self, record): + if self._with_profile: + record.profile = ConfigureStandard.get_profile() + do_color = self.with_colors and (self.can_colors or self.force_colors) + if ConfigureStandard._color_location: + # we copy raw formatting strings for color_* + # as formatting is handled in ansi_colors in this case + if do_color: + record.color_start = log.COLOR_START + record.color_end = log.COLOR_END + else: + record.color_start = record.color_end = "" + s = super(SatFormatter, self).format(record) + if do_color: + s = ConfigureStandard.ansi_colors(record.levelname, s) + return s + + self.formatterClass = SatFormatter + + def configure_output(self, output): + self.manage_outputs(output) + + def configure_logger(self, logger): + self.name_filter = log.FilterName(logger) if logger else None + + def configure_colors(self, colors, force_colors, levels_taints_dict): + super(ConfigureStandard, self).configure_colors( + colors, force_colors, levels_taints_dict + ) + self.formatterClass.with_colors = colors + self.formatterClass.force_colors = force_colors + if not colors and force_colors: + raise ValueError("force_colors can't be used if colors is False") + + def _add_handler(self, root_logger, hdlr, can_colors=False): + hdlr.setFormatter(self.formatterClass(can_colors)) + root_logger.addHandler(hdlr) + root_logger.setLevel(self.level) + if self.name_filter is not None: + hdlr.addFilter(self.name_filter) + + def post_treatment(self): + import logging + + root_logger = logging.getLogger() + if len(root_logger.handlers) == 0: + for handler, options in list(log.handlers.items()): + if handler == C.LOG_OPT_OUTPUT_DEFAULT: + hdlr = logging.StreamHandler() + try: + can_colors = hdlr.stream.isatty() + except AttributeError: + can_colors = False + self._add_handler(root_logger, hdlr, can_colors=can_colors) + elif handler == C.LOG_OPT_OUTPUT_MEMORY: + from logging.handlers import BufferingHandler + + class SatMemoryHandler(BufferingHandler): + def emit(self, record): + super(SatMemoryHandler, self).emit(self.format(record)) + + hdlr = SatMemoryHandler(options) + log.handlers[ + handler + ] = ( + hdlr + ) # we keep a reference to the handler to read the buffer later + self._add_handler(root_logger, hdlr, can_colors=False) + elif handler == C.LOG_OPT_OUTPUT_FILE: + import os.path + + for path in options: + hdlr = logging.FileHandler(os.path.expanduser(path)) + self._add_handler(root_logger, hdlr, can_colors=False) + else: + raise ValueError("Unknown handler type") + else: + root_logger.warning("Handlers already set on root logger") + + @staticmethod + def memory_get(size=None): + """Return buffered logs + + @param size: number of logs to return + """ + mem_handler = log.handlers[C.LOG_OPT_OUTPUT_MEMORY] + return ( + log_msg for log_msg in mem_handler.buffer[size if size is None else -size :] + ) + + +log.configure_cls[C.LOG_BACKEND_BASIC] = ConfigureBasic +log.configure_cls[C.LOG_BACKEND_TWISTED] = ConfigureTwisted +log.configure_cls[C.LOG_BACKEND_STANDARD] = ConfigureStandard + + +def configure(backend, **options): + """Configure logging behaviour + @param backend: can be: + C.LOG_BACKEND_STANDARD: use standard logging module + C.LOG_BACKEND_TWISTED: use twisted logging module (with standard logging observer) + C.LOG_BACKEND_BASIC: use a basic print based logging + C.LOG_BACKEND_CUSTOM: use a given Logger subclass + """ + return log.configure(backend, **options) + + +def _parse_options(options): + """Parse string options as given in conf or environment variable, and return expected python value + + @param options (dict): options with (key: name, value: string value) + """ + COLORS = C.LOG_OPT_COLORS[0] + LEVEL = C.LOG_OPT_LEVEL[0] + + if COLORS in options: + if options[COLORS].lower() in ("1", "true"): + options[COLORS] = True + elif options[COLORS] == "force": + options[COLORS] = True + options["force_colors"] = True + else: + options[COLORS] = False + if LEVEL in options: + level = options[LEVEL].upper() + if level not in C.LOG_LEVELS: + level = C.LOG_LVL_INFO + options[LEVEL] = level + + +def sat_configure(backend=C.LOG_BACKEND_STANDARD, const=None, backend_data=None): + """Configure logging system for SàT, can be used by frontends + + logs conf is read in SàT conf, then in environment variables. It must be done before Memory init + @param backend: backend to use, it can be: + - C.LOG_BACKEND_BASIC: print based backend + - C.LOG_BACKEND_TWISTED: Twisted logging backend + - C.LOG_BACKEND_STANDARD: standard logging backend + @param const: Const class to use instead of sat.core.constants.Const (mainly used to change default values) + """ + if const is not None: + global C + C = const + log.C = const + from libervia.backend.tools import config + import os + + log_conf = {} + sat_conf = config.parse_main_conf() + for opt_name, opt_default in C.LOG_OPTIONS(): + try: + log_conf[opt_name] = os.environ[ + "".join((C.ENV_PREFIX, C.LOG_OPT_PREFIX.upper(), opt_name.upper())) + ] + except KeyError: + log_conf[opt_name] = config.config_get( + sat_conf, C.LOG_OPT_SECTION, C.LOG_OPT_PREFIX + opt_name, opt_default + ) + + _parse_options(log_conf) + configure(backend, backend_data=backend_data, **log_conf) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/patches.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/patches.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,156 @@ +import copy +from twisted.words.protocols.jabber import xmlstream, sasl, client as tclient, jid +from wokkel import client +from libervia.backend.core.constants import Const as C +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) + +"""This module applies monkey patches to Twisted and Wokkel + First part handle certificate validation during XMPP connectionand are temporary + (until merged upstream). + Second part add a trigger point to send and onElement method of XmlStream + """ + + +## certificate validation patches + + +class XMPPClient(client.XMPPClient): + + def __init__(self, jid, password, host=None, port=5222, + tls_required=True, configurationForTLS=None): + self.jid = jid + self.domain = jid.host.encode('idna') + self.host = host + self.port = port + + factory = HybridClientFactory( + jid, password, tls_required=tls_required, + configurationForTLS=configurationForTLS) + + client.StreamManager.__init__(self, factory) + + +def HybridClientFactory(jid, password, tls_required=True, configurationForTLS=None): + a = HybridAuthenticator(jid, password, tls_required, configurationForTLS) + + return xmlstream.XmlStreamFactory(a) + + +class HybridAuthenticator(client.HybridAuthenticator): + res_binding = True + + def __init__(self, jid, password, tls_required=True, configurationForTLS=None): + xmlstream.ConnectAuthenticator.__init__(self, jid.host) + self.jid = jid + self.password = password + self.tls_required = tls_required + self.configurationForTLS = configurationForTLS + + def associateWithStream(self, xs): + xmlstream.ConnectAuthenticator.associateWithStream(self, xs) + + tlsInit = xmlstream.TLSInitiatingInitializer( + xs, required=self.tls_required, configurationForTLS=self.configurationForTLS) + xs.initializers = [client.client.CheckVersionInitializer(xs), + tlsInit, + CheckAuthInitializer(xs, self.res_binding)] + + +# XmlStream triggers + + +class XmlStream(xmlstream.XmlStream): + """XmlStream which allows to add hooks""" + + def __init__(self, authenticator): + xmlstream.XmlStream.__init__(self, authenticator) + # hooks at this level should not modify content + # so it's not needed to handle priority as with triggers + self._onElementHooks = [] + self._sendHooks = [] + + def add_hook(self, hook_type, callback): + """Add a send or receive hook""" + conflict_msg = f"Hook conflict: can't add {hook_type} hook {callback}" + if hook_type == C.STREAM_HOOK_RECEIVE: + if callback not in self._onElementHooks: + self._onElementHooks.append(callback) + else: + log.warning(conflict_msg) + elif hook_type == C.STREAM_HOOK_SEND: + if callback not in self._sendHooks: + self._sendHooks.append(callback) + else: + log.warning(conflict_msg) + else: + raise ValueError(f"Invalid hook type: {hook_type}") + + def onElement(self, element): + for hook in self._onElementHooks: + hook(element) + xmlstream.XmlStream.onElement(self, element) + + def send(self, obj): + for hook in self._sendHooks: + hook(obj) + xmlstream.XmlStream.send(self, obj) + + +# Binding activation (needed for stream management, XEP-0198) + + +class CheckAuthInitializer(client.CheckAuthInitializer): + + def __init__(self, xs, res_binding): + super(CheckAuthInitializer, self).__init__(xs) + self.res_binding = res_binding + + def initialize(self): + # XXX: modification of client.CheckAuthInitializer which has optional + # resource binding, and which doesn't do deprecated + # SessionInitializer + if (sasl.NS_XMPP_SASL, 'mechanisms') in self.xmlstream.features: + inits = [(sasl.SASLInitiatingInitializer, True)] + if self.res_binding: + inits.append((tclient.BindInitializer, True)), + + for initClass, required in inits: + init = initClass(self.xmlstream) + init.required = required + self.xmlstream.initializers.append(init) + elif (tclient.NS_IQ_AUTH_FEATURE, 'auth') in self.xmlstream.features: + self.xmlstream.initializers.append( + tclient.IQAuthInitializer(self.xmlstream)) + else: + raise Exception("No available authentication method found") + + +# jid fix + +def internJID(jidstring): + """ + Return interned JID. + + @rtype: L{JID} + """ + # XXX: this interJID return a copy of the cached jid + # this avoid modification of cached jid as JID is mutable + # TODO: propose this upstream + + if jidstring in jid.__internJIDs: + return copy.copy(jid.__internJIDs[jidstring]) + else: + j = jid.JID(jidstring) + jid.__internJIDs[jidstring] = j + return copy.copy(j) + + +def apply(): + # certificate validation + client.XMPPClient = XMPPClient + # XmlStream triggers + xmlstream.XmlStreamFactory.protocol = XmlStream + # jid fix + jid.internJID = internJID diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/sat_main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/sat_main.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1666 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys +import os.path +import uuid +import hashlib +import copy +from pathlib import Path +from typing import Optional, List, Tuple, Dict + +from wokkel.data_form import Option +from libervia import backend +from libervia.backend.core.i18n import _, D_, language_switch +from libervia.backend.core import patches +patches.apply() +from twisted.application import service +from twisted.internet import defer +from twisted.words.protocols.jabber import jid +from twisted.internet import reactor +from wokkel.xmppim import RosterItem +from libervia.backend.core import xmpp +from libervia.backend.core import exceptions +from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.log import getLogger + +from libervia.backend.core.constants import Const as C +from libervia.backend.memory import memory +from libervia.backend.memory import cache +from libervia.backend.memory import encryption +from libervia.backend.tools import async_trigger as trigger +from libervia.backend.tools import utils +from libervia.backend.tools import image +from libervia.backend.tools.common import dynamic_import +from libervia.backend.tools.common import regex +from libervia.backend.tools.common import data_format +from libervia.backend.stdui import ui_contact_list, ui_profile_manager +import libervia.backend.plugins + + +log = getLogger(__name__) + +class SAT(service.Service): + + def _init(self): + # we don't use __init__ to avoid doule initialisation with twistd + # this _init is called in startService + log.info(f"{C.APP_NAME} {self.full_version}") + self._cb_map = {} # map from callback_id to callbacks + # dynamic menus. key: callback_id, value: menu data (dictionnary) + self._menus = {} + self._menus_paths = {} # path to id. key: (menu_type, lower case tuple of path), + # value: menu id + self.initialised = defer.Deferred() + self.profiles = {} + self.plugins = {} + # map for short name to whole namespace, + # extended by plugins with register_namespace + self.ns_map = { + "x-data": xmpp.NS_X_DATA, + "disco#info": xmpp.NS_DISCO_INFO, + } + + self.memory = memory.Memory(self) + + # trigger are used to change Libervia behaviour + self.trigger = ( + trigger.TriggerManager() + ) + + bridge_name = ( + os.getenv("LIBERVIA_BRIDGE_NAME") + or self.memory.config_get("", "bridge", "dbus") + ) + + bridge_module = dynamic_import.bridge(bridge_name) + if bridge_module is None: + log.error(f"Can't find bridge module of name {bridge_name}") + sys.exit(1) + log.info(f"using {bridge_name} bridge") + try: + self.bridge = bridge_module.bridge() + except exceptions.BridgeInitError: + log.exception("bridge can't be initialised, can't start Libervia Backend") + sys.exit(1) + + defer.ensureDeferred(self._post_init()) + + @property + def version(self): + """Return the short version of Libervia""" + return C.APP_VERSION + + @property + def full_version(self): + """Return the full version of Libervia + + In developement mode, release name and extra data are returned too + """ + version = self.version + if version[-1] == "D": + # we are in debug version, we add extra data + try: + return self._version_cache + except AttributeError: + self._version_cache = "{} « {} » ({})".format( + version, C.APP_RELEASE_NAME, utils.get_repository_data(backend) + ) + return self._version_cache + else: + return version + + @property + def bridge_name(self): + return os.path.splitext(os.path.basename(self.bridge.__file__))[0] + + async def _post_init(self): + try: + bridge_pi = self.bridge.post_init + except AttributeError: + pass + else: + try: + await bridge_pi() + except Exception: + log.exception("Could not initialize bridge") + # because init is not complete at this stage, we use callLater + reactor.callLater(0, self.stop) + return + + self.bridge.register_method("ready_get", lambda: self.initialised) + self.bridge.register_method("version_get", lambda: self.full_version) + self.bridge.register_method("features_get", self.features_get) + self.bridge.register_method("profile_name_get", self.memory.get_profile_name) + self.bridge.register_method("profiles_list_get", self.memory.get_profiles_list) + self.bridge.register_method("entity_data_get", self.memory._get_entity_data) + self.bridge.register_method("entities_data_get", self.memory._get_entities_data) + self.bridge.register_method("profile_create", self.memory.create_profile) + self.bridge.register_method("profile_delete_async", self.memory.profile_delete_async) + self.bridge.register_method("profile_start_session", self.memory.start_session) + self.bridge.register_method( + "profile_is_session_started", self.memory._is_session_started + ) + self.bridge.register_method("profile_set_default", self.memory.profile_set_default) + self.bridge.register_method("connect", self._connect) + self.bridge.register_method("disconnect", self.disconnect) + self.bridge.register_method("contact_get", self._contact_get) + self.bridge.register_method("contacts_get", self.contacts_get) + self.bridge.register_method("contacts_get_from_group", self.contacts_get_from_group) + self.bridge.register_method("main_resource_get", self.memory._get_main_resource) + self.bridge.register_method( + "presence_statuses_get", self.memory._get_presence_statuses + ) + self.bridge.register_method("sub_waiting_get", self.memory.sub_waiting_get) + self.bridge.register_method("message_send", self._message_send) + self.bridge.register_method("message_encryption_start", + self._message_encryption_start) + self.bridge.register_method("message_encryption_stop", + self._message_encryption_stop) + self.bridge.register_method("message_encryption_get", + self._message_encryption_get) + self.bridge.register_method("encryption_namespace_get", + self._encryption_namespace_get) + self.bridge.register_method("encryption_plugins_get", self._encryption_plugins_get) + self.bridge.register_method("encryption_trust_ui_get", self._encryption_trust_ui_get) + self.bridge.register_method("config_get", self._get_config) + self.bridge.register_method("param_set", self.param_set) + self.bridge.register_method("param_get_a", self.memory.get_string_param_a) + self.bridge.register_method("private_data_get", self.memory._private_data_get) + self.bridge.register_method("private_data_set", self.memory._private_data_set) + self.bridge.register_method("private_data_delete", self.memory._private_data_delete) + self.bridge.register_method("param_get_a_async", self.memory.async_get_string_param_a) + self.bridge.register_method( + "params_values_from_category_get_async", + self.memory._get_params_values_from_category, + ) + self.bridge.register_method("param_ui_get", self.memory._get_params_ui) + self.bridge.register_method( + "params_categories_get", self.memory.params_categories_get + ) + self.bridge.register_method("params_register_app", self.memory.params_register_app) + self.bridge.register_method("history_get", self.memory._history_get) + self.bridge.register_method("presence_set", self._set_presence) + self.bridge.register_method("subscription", self.subscription) + self.bridge.register_method("contact_add", self._add_contact) + self.bridge.register_method("contact_update", self._update_contact) + self.bridge.register_method("contact_del", self._del_contact) + self.bridge.register_method("roster_resync", self._roster_resync) + self.bridge.register_method("is_connected", self.is_connected) + self.bridge.register_method("action_launch", self._action_launch) + self.bridge.register_method("actions_get", self.actions_get) + self.bridge.register_method("progress_get", self._progress_get) + self.bridge.register_method("progress_get_all", self._progress_get_all) + self.bridge.register_method("menus_get", self.get_menus) + self.bridge.register_method("menu_help_get", self.get_menu_help) + self.bridge.register_method("menu_launch", self._launch_menu) + self.bridge.register_method("disco_infos", self.memory.disco._disco_infos) + self.bridge.register_method("disco_items", self.memory.disco._disco_items) + self.bridge.register_method("disco_find_by_features", self._find_by_features) + self.bridge.register_method("params_template_save", self.memory.save_xml) + self.bridge.register_method("params_template_load", self.memory.load_xml) + self.bridge.register_method("session_infos_get", self.get_session_infos) + self.bridge.register_method("devices_infos_get", self._get_devices_infos) + self.bridge.register_method("namespaces_get", self.get_namespaces) + self.bridge.register_method("image_check", self._image_check) + self.bridge.register_method("image_resize", self._image_resize) + self.bridge.register_method("image_generate_preview", self._image_generate_preview) + self.bridge.register_method("image_convert", self._image_convert) + + + await self.memory.initialise() + self.common_cache = cache.Cache(self, None) + log.info(_("Memory initialised")) + try: + self._import_plugins() + ui_contact_list.ContactList(self) + ui_profile_manager.ProfileManager(self) + except Exception as e: + log.error(f"Could not initialize backend: {e}") + sys.exit(1) + self._add_base_menus() + + self.initialised.callback(None) + log.info(_("Backend is ready")) + + # profile autoconnection must be done after self.initialised is called because + # start_session waits for it. + autoconnect_dict = await self.memory.storage.get_ind_param_values( + category='Connection', name='autoconnect_backend', + ) + profiles_autoconnect = [p for p, v in autoconnect_dict.items() if C.bool(v)] + if not self.trigger.point("profilesAutoconnect", profiles_autoconnect): + return + if profiles_autoconnect: + log.info(D_( + "Following profiles will be connected automatically: {profiles}" + ).format(profiles= ', '.join(profiles_autoconnect))) + connect_d_list = [] + for profile in profiles_autoconnect: + connect_d_list.append(defer.ensureDeferred(self.connect(profile))) + + if connect_d_list: + results = await defer.DeferredList(connect_d_list) + for idx, (success, result) in enumerate(results): + if not success: + profile = profiles_autoconnect[0] + log.warning( + _("Can't autoconnect profile {profile}: {reason}").format( + profile = profile, + reason = result) + ) + + def _add_base_menus(self): + """Add base menus""" + encryption.EncryptionHandler._import_menus(self) + + def _unimport_plugin(self, plugin_path): + """remove a plugin from sys.modules if it is there""" + try: + del sys.modules[plugin_path] + except KeyError: + pass + + def _import_plugins(self): + """import all plugins found in plugins directory""" + # FIXME: module imported but cancelled should be deleted + # TODO: make this more generic and reusable in tools.common + # FIXME: should use imp + # TODO: do not import all plugins if no needed: component plugins are not needed + # if we just use a client, and plugin blacklisting should be possible in + # libervia.conf + plugins_path = Path(libervia.backend.plugins.__file__).parent + plugins_to_import = {} # plugins we still have to import + for plug_path in plugins_path.glob("plugin_*"): + if plug_path.is_dir(): + init_path = plug_path / f"__init__.{C.PLUGIN_EXT}" + if not init_path.exists(): + log.warning( + f"{plug_path} doesn't appear to be a package, can't load it") + continue + plug_name = plug_path.name + elif plug_path.is_file(): + if plug_path.suffix != f".{C.PLUGIN_EXT}": + continue + plug_name = plug_path.stem + else: + log.warning( + f"{plug_path} is not a file or a dir, ignoring it") + continue + if not plug_name.isidentifier(): + log.warning( + f"{plug_name!r} is not a valid name for a plugin, ignoring it") + continue + plugin_path = f"libervia.backend.plugins.{plug_name}" + try: + __import__(plugin_path) + except exceptions.MissingModule as e: + self._unimport_plugin(plugin_path) + log.warning( + "Can't import plugin [{path}] because of an unavailale third party " + "module:\n{msg}".format( + path=plugin_path, msg=e + ) + ) + continue + except exceptions.CancelError as e: + log.info( + "Plugin [{path}] cancelled its own import: {msg}".format( + path=plugin_path, msg=e + ) + ) + self._unimport_plugin(plugin_path) + continue + except Exception: + import traceback + + log.error( + _("Can't import plugin [{path}]:\n{error}").format( + path=plugin_path, error=traceback.format_exc() + ) + ) + self._unimport_plugin(plugin_path) + continue + mod = sys.modules[plugin_path] + plugin_info = mod.PLUGIN_INFO + import_name = plugin_info["import_name"] + + plugin_modes = plugin_info["modes"] = set( + plugin_info.setdefault("modes", C.PLUG_MODE_DEFAULT) + ) + if not plugin_modes.intersection(C.PLUG_MODE_BOTH): + log.error( + f"Can't import plugin at {plugin_path}, invalid {C.PI_MODES!r} " + f"value: {plugin_modes!r}" + ) + continue + + # if the plugin is an entry point, it must work in component mode + if plugin_info["type"] == C.PLUG_TYPE_ENTRY_POINT: + # if plugin is an entrypoint, we cache it + if C.PLUG_MODE_COMPONENT not in plugin_modes: + log.error( + _( + "{type} type must be used with {mode} mode, ignoring plugin" + ).format(type=C.PLUG_TYPE_ENTRY_POINT, mode=C.PLUG_MODE_COMPONENT) + ) + self._unimport_plugin(plugin_path) + continue + + if import_name in plugins_to_import: + log.error( + _( + "Name conflict for import name [{import_name}], can't import " + "plugin [{name}]" + ).format(**plugin_info) + ) + continue + plugins_to_import[import_name] = (plugin_path, mod, plugin_info) + while True: + try: + self._import_plugins_from_dict(plugins_to_import) + except ImportError: + pass + if not plugins_to_import: + break + + def _import_plugins_from_dict( + self, plugins_to_import, import_name=None, optional=False + ): + """Recursively import and their dependencies in the right order + + @param plugins_to_import(dict): key=import_name and values=(plugin_path, module, + plugin_info) + @param import_name(unicode, None): name of the plugin to import as found in + PLUGIN_INFO['import_name'] + @param optional(bool): if False and plugin is not found, an ImportError exception + is raised + """ + if import_name in self.plugins: + log.debug("Plugin {} already imported, passing".format(import_name)) + return + if not import_name: + import_name, (plugin_path, mod, plugin_info) = plugins_to_import.popitem() + else: + if not import_name in plugins_to_import: + if optional: + log.warning( + _("Recommended plugin not found: {}").format(import_name) + ) + return + msg = "Dependency not found: {}".format(import_name) + log.error(msg) + raise ImportError(msg) + plugin_path, mod, plugin_info = plugins_to_import.pop(import_name) + dependencies = plugin_info.setdefault("dependencies", []) + recommendations = plugin_info.setdefault("recommendations", []) + for to_import in dependencies + recommendations: + if to_import not in self.plugins: + log.debug( + "Recursively import dependency of [%s]: [%s]" + % (import_name, to_import) + ) + try: + self._import_plugins_from_dict( + plugins_to_import, to_import, to_import not in dependencies + ) + except ImportError as e: + log.warning( + _("Can't import plugin {name}: {error}").format( + name=plugin_info["name"], error=e + ) + ) + if optional: + return + raise e + log.info("importing plugin: {}".format(plugin_info["name"])) + # we instanciate the plugin here + try: + self.plugins[import_name] = getattr(mod, plugin_info["main"])(self) + except Exception as e: + log.exception( + f"Can't load plugin \"{plugin_info['name']}\", ignoring it: {e}" + ) + if optional: + return + raise ImportError("Error during initiation") + if C.bool(plugin_info.get(C.PI_HANDLER, C.BOOL_FALSE)): + self.plugins[import_name].is_handler = True + else: + self.plugins[import_name].is_handler = False + # we keep metadata as a Class attribute + self.plugins[import_name]._info = plugin_info + # TODO: test xmppclient presence and register handler parent + + def plugins_unload(self): + """Call unload method on every loaded plugin, if exists + + @return (D): A deferred which return None when all method have been called + """ + # TODO: in the futur, it should be possible to hot unload a plugin + # pluging depending on the unloaded one should be unloaded too + # for now, just a basic call on plugin.unload is done + defers_list = [] + for plugin in self.plugins.values(): + try: + unload = plugin.unload + except AttributeError: + continue + else: + defers_list.append(utils.as_deferred(unload)) + return defers_list + + def _connect(self, profile_key, password="", options=None): + profile = self.memory.get_profile_name(profile_key) + return defer.ensureDeferred(self.connect(profile, password, options)) + + async def connect( + self, profile, password="", options=None, max_retries=C.XMPP_MAX_RETRIES): + """Connect a profile (i.e. connect client.component to XMPP server) + + Retrieve the individual parameters, authenticate the profile + and initiate the connection to the associated XMPP server. + @param profile: %(doc_profile)s + @param password (string): the Libervia profile password + @param options (dict): connection options. Key can be: + - + @param max_retries (int): max number of connection retries + @return (D(bool)): + - True if the XMPP connection was already established + - False if the XMPP connection has been initiated (it may still fail) + @raise exceptions.PasswordError: Profile password is wrong + """ + if options is None: + options = {} + + await self.memory.start_session(password, profile) + + if self.is_connected(profile): + log.info(_("already connected !")) + return True + + if self.memory.is_component(profile): + await xmpp.SatXMPPComponent.start_connection(self, profile, max_retries) + else: + await xmpp.SatXMPPClient.start_connection(self, profile, max_retries) + + return False + + def disconnect(self, profile_key): + """disconnect from jabber server""" + # FIXME: client should not be deleted if only disconnected + # it shoud be deleted only when session is finished + if not self.is_connected(profile_key): + # is_connected is checked here and not on client + # because client is deleted when session is ended + log.info(_("not connected !")) + return defer.succeed(None) + client = self.get_client(profile_key) + return client.entity_disconnect() + + def features_get(self, profile_key=C.PROF_KEY_NONE): + """Get available features + + Return list of activated plugins and plugin specific data + @param profile_key: %(doc_profile_key)s + C.PROF_KEY_NONE can be used to have general plugins data (i.e. not profile + dependent) + @return (dict)[Deferred]: features data where: + - key is plugin import name, present only for activated plugins + - value is a an other dict, when meaning is specific to each plugin. + this dict is return by plugin's getFeature method. + If this method doesn't exists, an empty dict is returned. + """ + try: + # FIXME: there is no method yet to check profile session + # as soon as one is implemented, it should be used here + self.get_client(profile_key) + except KeyError: + log.warning("Requesting features for a profile outside a session") + profile_key = C.PROF_KEY_NONE + except exceptions.ProfileNotSetError: + pass + + features = [] + for import_name, plugin in self.plugins.items(): + try: + features_d = utils.as_deferred(plugin.features_get, profile_key) + except AttributeError: + features_d = defer.succeed({}) + features.append(features_d) + + d_list = defer.DeferredList(features) + + def build_features(result, import_names): + assert len(result) == len(import_names) + ret = {} + for name, (success, data) in zip(import_names, result): + if success: + ret[name] = data + else: + log.warning( + "Error while getting features for {name}: {failure}".format( + name=name, failure=data + ) + ) + ret[name] = {} + return ret + + d_list.addCallback(build_features, list(self.plugins.keys())) + return d_list + + def _contact_get(self, entity_jid_s, profile_key): + client = self.get_client(profile_key) + entity_jid = jid.JID(entity_jid_s) + return defer.ensureDeferred(self.get_contact(client, entity_jid)) + + async def get_contact(self, client, entity_jid): + # we want to be sure that roster has been received + await client.roster.got_roster + item = client.roster.get_item(entity_jid) + if item is None: + raise exceptions.NotFound(f"{entity_jid} is not in roster!") + return (client.roster.get_attributes(item), list(item.groups)) + + def contacts_get(self, profile_key): + client = self.get_client(profile_key) + + def got_roster(__): + ret = [] + for item in client.roster.get_items(): # we get all items for client's roster + # and convert them to expected format + attr = client.roster.get_attributes(item) + # we use full() and not userhost() because jid with resources are allowed + # in roster, even if it's not common. + ret.append([item.entity.full(), attr, list(item.groups)]) + return ret + + return client.roster.got_roster.addCallback(got_roster) + + def contacts_get_from_group(self, group, profile_key): + client = self.get_client(profile_key) + return [jid_.full() for jid_ in client.roster.get_jids_from_group(group)] + + def purge_entity(self, profile): + """Remove reference to a profile client/component and purge cache + + the garbage collector can then free the memory + """ + try: + del self.profiles[profile] + except KeyError: + log.error(_("Trying to remove reference to a client not referenced")) + else: + self.memory.purge_profile_session(profile) + + def startService(self): + self._init() + log.info("Salut à toi ô mon frère !") + + def stopService(self): + log.info("Salut aussi à Rantanplan") + return self.plugins_unload() + + def run(self): + log.debug(_("running app")) + reactor.run() + + def stop(self): + log.debug(_("stopping app")) + reactor.stop() + + ## Misc methods ## + + def get_jid_n_stream(self, profile_key): + """Convenient method to get jid and stream from profile key + @return: tuple (jid, xmlstream) from profile, can be None""" + # TODO: deprecate this method (get_client is enough) + profile = self.memory.get_profile_name(profile_key) + if not profile or not self.profiles[profile].is_connected(): + return (None, None) + return (self.profiles[profile].jid, self.profiles[profile].xmlstream) + + def get_client(self, profile_key: str) -> xmpp.SatXMPPClient: + """Convenient method to get client from profile key + + @return: the client + @raise exceptions.ProfileKeyUnknown: the profile or profile key doesn't exist + @raise exceptions.NotFound: client is not available + This happen if profile has not been used yet + """ + profile = self.memory.get_profile_name(profile_key) + if not profile: + raise exceptions.ProfileKeyUnknown + try: + return self.profiles[profile] + except KeyError: + raise exceptions.NotFound(profile_key) + + def get_clients(self, profile_key): + """Convenient method to get list of clients from profile key + + Manage list through profile_key like C.PROF_KEY_ALL + @param profile_key: %(doc_profile_key)s + @return: list of clients + """ + if not profile_key: + raise exceptions.DataError(_("profile_key must not be empty")) + try: + profile = self.memory.get_profile_name(profile_key, True) + except exceptions.ProfileUnknownError: + return [] + if profile == C.PROF_KEY_ALL: + return list(self.profiles.values()) + elif profile[0] == "@": #  only profile keys can start with "@" + raise exceptions.ProfileKeyUnknown + return [self.profiles[profile]] + + def _get_config(self, section, name): + """Get the main configuration option + + @param section: section of the config file (None or '' for DEFAULT) + @param name: name of the option + @return: unicode representation of the option + """ + return str(self.memory.config_get(section, name, "")) + + def log_errback(self, failure_, msg=_("Unexpected error: {failure_}")): + """Generic errback logging + + @param msg(unicode): error message ("failure_" key will be use for format) + can be used as last errback to show unexpected error + """ + log.error(msg.format(failure_=failure_)) + return failure_ + + #  namespaces + + def register_namespace(self, short_name, namespace): + """associate a namespace to a short name""" + if short_name in self.ns_map: + raise exceptions.ConflictError("this short name is already used") + log.debug(f"registering namespace {short_name} => {namespace}") + self.ns_map[short_name] = namespace + + def get_namespaces(self): + return self.ns_map + + def get_namespace(self, short_name): + try: + return self.ns_map[short_name] + except KeyError: + raise exceptions.NotFound("namespace {short_name} is not registered" + .format(short_name=short_name)) + + def get_session_infos(self, profile_key): + """compile interesting data on current profile session""" + client = self.get_client(profile_key) + data = { + "jid": client.jid.full(), + "started": str(int(client.started)) + } + return defer.succeed(data) + + def _get_devices_infos(self, bare_jid, profile_key): + client = self.get_client(profile_key) + if not bare_jid: + bare_jid = None + d = defer.ensureDeferred(self.get_devices_infos(client, bare_jid)) + d.addCallback(lambda data: data_format.serialise(data)) + return d + + async def get_devices_infos(self, client, bare_jid=None): + """compile data on an entity devices + + @param bare_jid(jid.JID, None): bare jid of entity to check + None to use client own jid + @return (list[dict]): list of data, one item per resource. + Following keys can be set: + - resource(str): resource name + """ + own_jid = client.jid.userhostJID() + if bare_jid is None: + bare_jid = own_jid + else: + bare_jid = jid.JID(bare_jid) + resources = self.memory.get_all_resources(client, bare_jid) + if bare_jid == own_jid: + # our own jid is not stored in memory's cache + resources.add(client.jid.resource) + ret_data = [] + for resource in resources: + res_jid = copy.copy(bare_jid) + res_jid.resource = resource + cache_data = self.memory.entity_data_get(client, res_jid) + res_data = { + "resource": resource, + } + try: + presence = cache_data['presence'] + except KeyError: + pass + else: + res_data['presence'] = { + "show": presence.show, + "priority": presence.priority, + "statuses": presence.statuses, + } + + disco = await self.get_disco_infos(client, res_jid) + + for (category, type_), name in disco.identities.items(): + identities = res_data.setdefault('identities', []) + identities.append({ + "name": name, + "category": category, + "type": type_, + }) + + ret_data.append(res_data) + + return ret_data + + # images + + def _image_check(self, path): + report = image.check(self, path) + return data_format.serialise(report) + + def _image_resize(self, path, width, height): + d = image.resize(path, (width, height)) + d.addCallback(lambda new_image_path: str(new_image_path)) + return d + + def _image_generate_preview(self, path, profile_key): + client = self.get_client(profile_key) + d = defer.ensureDeferred(self.image_generate_preview(client, Path(path))) + d.addCallback(lambda preview_path: str(preview_path)) + return d + + async def image_generate_preview(self, client, path): + """Helper method to generate in cache a preview of an image + + @param path(Path): path to the image + @return (Path): path to the generated preview + """ + report = image.check(self, path, max_size=(300, 300)) + + if not report['too_large']: + # in the unlikely case that image is already smaller than a preview + preview_path = path + else: + # we use hash as id, to re-use potentially existing preview + path_hash = hashlib.sha256(str(path).encode()).hexdigest() + uid = f"{path.stem}_{path_hash}_preview" + filename = f"{uid}{path.suffix.lower()}" + metadata = client.cache.get_metadata(uid=uid) + if metadata is not None: + preview_path = metadata['path'] + else: + with client.cache.cache_data( + source='HOST_PREVIEW', + uid=uid, + filename=filename) as cache_f: + + preview_path = await image.resize( + path, + new_size=report['recommended_size'], + dest=cache_f + ) + + return preview_path + + def _image_convert(self, source, dest, extra, profile_key): + client = self.get_client(profile_key) if profile_key else None + source = Path(source) + dest = None if not dest else Path(dest) + extra = data_format.deserialise(extra) + d = defer.ensureDeferred(self.image_convert(client, source, dest, extra)) + d.addCallback(lambda dest_path: str(dest_path)) + return d + + async def image_convert(self, client, source, dest=None, extra=None): + """Helper method to convert an image from one format to an other + + @param client(SatClient, None): client to use for caching + this parameter is only used if dest is None + if client is None, common cache will be used insted of profile cache + @param source(Path): path to the image to convert + @param dest(None, Path, file): where to save the converted file + - None: use a cache file (uid generated from hash of source) + file will be converted to PNG + - Path: path to the file to create/overwrite + - file: a file object which must be opened for writing in binary mode + @param extra(dict, None): conversion options + see [image.convert] for details + @return (Path): path to the converted image + @raise ValueError: an issue happened with source of dest + """ + if not source.is_file: + raise ValueError(f"Source file {source} doesn't exist!") + if dest is None: + # we use hash as id, to re-use potentially existing conversion + path_hash = hashlib.sha256(str(source).encode()).hexdigest() + uid = f"{source.stem}_{path_hash}_convert_png" + filename = f"{uid}.png" + if client is None: + cache = self.common_cache + else: + cache = client.cache + metadata = cache.get_metadata(uid=uid) + if metadata is not None: + # there is already a conversion for this image in cache + return metadata['path'] + else: + with cache.cache_data( + source='HOST_IMAGE_CONVERT', + uid=uid, + filename=filename) as cache_f: + + converted_path = await image.convert( + source, + dest=cache_f, + extra=extra + ) + return converted_path + else: + return await image.convert(source, dest, extra) + + + # local dirs + + def get_local_path( + self, + client: Optional[SatXMPPEntity], + dir_name: str, + *extra_path: str, + component: bool = False, + ) -> Path: + """Retrieve path for local data + + if path doesn't exist, it will be created + @param client: client instance + if not none, client.profile will be used as last path element + @param dir_name: name of the main path directory + @param *extra_path: extra path element(s) to use + @param component: if True, path will be prefixed with C.COMPONENTS_DIR + @return: path + """ + local_dir = self.memory.config_get("", "local_dir") + if not local_dir: + raise exceptions.InternalError("local_dir must be set") + path_elts = [] + if component: + path_elts.append(C.COMPONENTS_DIR) + path_elts.append(regex.path_escape(dir_name)) + if extra_path: + path_elts.extend([regex.path_escape(p) for p in extra_path]) + if client is not None: + path_elts.append(regex.path_escape(client.profile)) + local_path = Path(*path_elts) + local_path.mkdir(0o700, parents=True, exist_ok=True) + return local_path + + ## Client management ## + + def param_set(self, name, value, category, security_limit, profile_key): + """set wanted paramater and notice observers""" + self.memory.param_set(name, value, category, security_limit, profile_key) + + def is_connected(self, profile_key): + """Return connection status of profile + + @param profile_key: key_word or profile name to determine profile name + @return: True if connected + """ + profile = self.memory.get_profile_name(profile_key) + if not profile: + log.error(_("asking connection status for a non-existant profile")) + raise exceptions.ProfileUnknownError(profile_key) + if profile not in self.profiles: + return False + return self.profiles[profile].is_connected() + + ## Encryption ## + + def register_encryption_plugin(self, *args, **kwargs): + return encryption.EncryptionHandler.register_plugin(*args, **kwargs) + + def _message_encryption_start(self, to_jid_s, namespace, replace=False, + profile_key=C.PROF_KEY_NONE): + client = self.get_client(profile_key) + to_jid = jid.JID(to_jid_s) + return defer.ensureDeferred( + client.encryption.start(to_jid, namespace or None, replace)) + + def _message_encryption_stop(self, to_jid_s, profile_key=C.PROF_KEY_NONE): + client = self.get_client(profile_key) + to_jid = jid.JID(to_jid_s) + return defer.ensureDeferred( + client.encryption.stop(to_jid)) + + def _message_encryption_get(self, to_jid_s, profile_key=C.PROF_KEY_NONE): + client = self.get_client(profile_key) + to_jid = jid.JID(to_jid_s) + session_data = client.encryption.getSession(to_jid) + return client.encryption.get_bridge_data(session_data) + + def _encryption_namespace_get(self, name): + return encryption.EncryptionHandler.get_ns_from_name(name) + + def _encryption_plugins_get(self): + plugins = encryption.EncryptionHandler.getPlugins() + ret = [] + for p in plugins: + ret.append({ + "name": p.name, + "namespace": p.namespace, + "priority": p.priority, + "directed": p.directed, + }) + return data_format.serialise(ret) + + def _encryption_trust_ui_get(self, to_jid_s, namespace, profile_key): + client = self.get_client(profile_key) + to_jid = jid.JID(to_jid_s) + d = defer.ensureDeferred( + client.encryption.get_trust_ui(to_jid, namespace=namespace or None)) + d.addCallback(lambda xmlui: xmlui.toXml()) + return d + + ## XMPP methods ## + + def _message_send( + self, to_jid_s, message, subject=None, mess_type="auto", extra_s="", + profile_key=C.PROF_KEY_NONE): + client = self.get_client(profile_key) + to_jid = jid.JID(to_jid_s) + return client.sendMessage( + to_jid, + message, + subject, + mess_type, + data_format.deserialise(extra_s) + ) + + def _set_presence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): + return self.presence_set(jid.JID(to) if to else None, show, statuses, profile_key) + + def presence_set(self, to_jid=None, show="", statuses=None, + profile_key=C.PROF_KEY_NONE): + """Send our presence information""" + if statuses is None: + statuses = {} + profile = self.memory.get_profile_name(profile_key) + assert profile + priority = int( + self.memory.param_get_a("Priority", "Connection", profile_key=profile) + ) + self.profiles[profile].presence.available(to_jid, show, statuses, priority) + # XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not + # broadcasted to generating resource) + if "" in statuses: + statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop("") + self.bridge.presence_update( + self.profiles[profile].jid.full(), show, int(priority), statuses, profile + ) + + def subscription(self, subs_type, raw_jid, profile_key): + """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.get_profile_name(profile_key) + assert profile + to_jid = jid.JID(raw_jid) + log.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) + elif subs_type == "unsubscribe": + self.profiles[profile].presence.unsubscribe(to_jid) + elif subs_type == "unsubscribed": + self.profiles[profile].presence.unsubscribed(to_jid) + + def _add_contact(self, to_jid_s, profile_key): + return self.contact_add(jid.JID(to_jid_s), profile_key) + + def contact_add(self, to_jid, profile_key): + """Add a contact in roster list""" + profile = self.memory.get_profile_name(profile_key) + assert profile + # presence is sufficient, as a roster push will be sent according to + # RFC 6121 §3.1.2 + self.profiles[profile].presence.subscribe(to_jid) + + def _update_contact(self, to_jid_s, name, groups, profile_key): + client = self.get_client(profile_key) + return self.contact_update(client, jid.JID(to_jid_s), name, groups) + + def contact_update(self, client, to_jid, name, groups): + """update a contact in roster list""" + roster_item = RosterItem(to_jid) + roster_item.name = name or u'' + roster_item.groups = set(groups) + if not self.trigger.point("roster_update", client, roster_item): + return + return client.roster.setItem(roster_item) + + def _del_contact(self, to_jid_s, profile_key): + return self.contact_del(jid.JID(to_jid_s), profile_key) + + def contact_del(self, to_jid, profile_key): + """Remove contact from roster list""" + profile = self.memory.get_profile_name(profile_key) + assert profile + self.profiles[profile].presence.unsubscribe(to_jid) # is not asynchronous + return self.profiles[profile].roster.removeItem(to_jid) + + def _roster_resync(self, profile_key): + client = self.get_client(profile_key) + return client.roster.resync() + + ## Discovery ## + # discovery methods are shortcuts to self.memory.disco + # the main difference with client.disco is that self.memory.disco manage cache + + def hasFeature(self, *args, **kwargs): + return self.memory.disco.hasFeature(*args, **kwargs) + + def check_feature(self, *args, **kwargs): + return self.memory.disco.check_feature(*args, **kwargs) + + def check_features(self, *args, **kwargs): + return self.memory.disco.check_features(*args, **kwargs) + + def has_identity(self, *args, **kwargs): + return self.memory.disco.has_identity(*args, **kwargs) + + def get_disco_infos(self, *args, **kwargs): + return self.memory.disco.get_infos(*args, **kwargs) + + def getDiscoItems(self, *args, **kwargs): + return self.memory.disco.get_items(*args, **kwargs) + + def find_service_entity(self, *args, **kwargs): + return self.memory.disco.find_service_entity(*args, **kwargs) + + def find_service_entities(self, *args, **kwargs): + return self.memory.disco.find_service_entities(*args, **kwargs) + + def find_features_set(self, *args, **kwargs): + return self.memory.disco.find_features_set(*args, **kwargs) + + def _find_by_features(self, namespaces, identities, bare_jids, service, roster, own_jid, + local_device, profile_key): + client = self.get_client(profile_key) + identities = [tuple(i) for i in identities] if identities else None + return defer.ensureDeferred(self.find_by_features( + client, namespaces, identities, bare_jids, service, roster, own_jid, + local_device)) + + async def find_by_features( + self, + client: SatXMPPEntity, + namespaces: List[str], + identities: Optional[List[Tuple[str, str]]]=None, + bare_jids: bool=False, + service: bool=True, + roster: bool=True, + own_jid: bool=True, + local_device: bool=False + ) -> Tuple[ + Dict[jid.JID, Tuple[str, str, str]], + Dict[jid.JID, Tuple[str, str, str]], + Dict[jid.JID, Tuple[str, str, str]] + ]: + """Retrieve all services or contacts managing a set a features + + @param namespaces: features which must be handled + @param identities: if not None or empty, + only keep those identities + tuple must be (category, type) + @param bare_jids: retrieve only bare_jids if True + if False, retrieve full jid of connected devices + @param service: if True return service from our server + @param roster: if True, return entities in roster + full jid of all matching resources available will be returned + @param own_jid: if True, return profile's jid resources + @param local_device: if True, return profile's jid local resource + (i.e. client.jid) + @return: found entities in a tuple with: + - service entities + - own entities + - roster entities + Each element is a dict mapping from jid to a tuple with category, type and + name of the entity + """ + assert isinstance(namespaces, list) + if not identities: + identities = None + if not namespaces and not identities: + raise exceptions.DataError( + "at least one namespace or one identity must be set" + ) + found_service = {} + found_own = {} + found_roster = {} + if service: + services_jids = await self.find_features_set(client, namespaces) + services_jids = list(services_jids) # we need a list to map results below + services_infos = await defer.DeferredList( + [self.get_disco_infos(client, service_jid) for service_jid in services_jids] + ) + + for idx, (success, infos) in enumerate(services_infos): + service_jid = services_jids[idx] + if not success: + log.warning( + _("Can't find features for service {service_jid}, ignoring") + .format(service_jid=service_jid.full())) + continue + if (identities is not None + and not set(infos.identities.keys()).issuperset(identities)): + continue + found_identities = [ + (cat, type_, name or "") + for (cat, type_), name in infos.identities.items() + ] + found_service[service_jid.full()] = found_identities + + to_find = [] + if own_jid: + to_find.append((found_own, [client.jid.userhostJID()])) + if roster: + to_find.append((found_roster, client.roster.get_jids())) + + for found, jids in to_find: + full_jids = [] + disco_defers = [] + + for jid_ in jids: + if jid_.resource: + if bare_jids: + continue + resources = [jid_.resource] + else: + if bare_jids: + resources = [None] + else: + try: + resources = self.memory.get_available_resources(client, jid_) + except exceptions.UnknownEntityError: + continue + if not resources and jid_ == client.jid.userhostJID() and own_jid: + # small hack to avoid missing our own resource when this + # method is called at the very beginning of the session + # and our presence has not been received yet + resources = [client.jid.resource] + for resource in resources: + full_jid = jid.JID(tuple=(jid_.user, jid_.host, resource)) + if full_jid == client.jid and not local_device: + continue + full_jids.append(full_jid) + + disco_defers.append(self.get_disco_infos(client, full_jid)) + + d_list = defer.DeferredList(disco_defers) + # XXX: 10 seconds may be too low for slow connections (e.g. mobiles) + # but for discovery, that's also the time the user will wait the first time + # before seing the page, if something goes wrong. + d_list.addTimeout(10, reactor) + infos_data = await d_list + + for idx, (success, infos) in enumerate(infos_data): + full_jid = full_jids[idx] + if not success: + log.warning( + _("Can't retrieve {full_jid} infos, ignoring") + .format(full_jid=full_jid.full())) + continue + if infos.features.issuperset(namespaces): + if identities is not None and not set( + infos.identities.keys() + ).issuperset(identities): + continue + found_identities = [ + (cat, type_, name or "") + for (cat, type_), name in infos.identities.items() + ] + found[full_jid.full()] = found_identities + + return (found_service, found_own, found_roster) + + ## Generic HMI ## + + def _kill_action(self, keep_id, client): + log.debug("Killing action {} for timeout".format(keep_id)) + client.actions[keep_id] + + def action_new( + self, + action_data, + security_limit=C.NO_SECURITY_LIMIT, + keep_id=None, + profile=C.PROF_KEY_NONE, + ): + """Shortcut to bridge.action_new which generate an id and keep for retrieval + + @param action_data(dict): action data (see bridge documentation) + @param security_limit: %(doc_security_limit)s + @param keep_id(None, unicode): if not None, used to keep action for differed + retrieval. The value will be used as callback_id, be sure to use an unique + value. + Action will be deleted after 30 min. + @param profile: %(doc_profile)s + """ + if keep_id is not None: + id_ = keep_id + client = self.get_client(profile) + action_timer = reactor.callLater(60 * 30, self._kill_action, keep_id, client) + client.actions[keep_id] = (action_data, id_, security_limit, action_timer) + else: + id_ = str(uuid.uuid4()) + + self.bridge.action_new( + data_format.serialise(action_data), id_, security_limit, profile + ) + + def actions_get(self, profile): + """Return current non answered actions + + @param profile: %(doc_profile)s + """ + client = self.get_client(profile) + return [ + (data_format.serialise(action_tuple[0]), *action_tuple[1:-1]) + for action_tuple in client.actions.values() + ] + + def register_progress_cb( + self, progress_id, callback, metadata=None, profile=C.PROF_KEY_NONE + ): + """Register a callback called when progress is requested for id""" + if metadata is None: + metadata = {} + client = self.get_client(profile) + if progress_id in client._progress_cb: + raise exceptions.ConflictError("Progress ID is not unique !") + client._progress_cb[progress_id] = (callback, metadata) + + def remove_progress_cb(self, progress_id, profile): + """Remove a progress callback""" + client = self.get_client(profile) + try: + del client._progress_cb[progress_id] + except KeyError: + log.error(_("Trying to remove an unknow progress callback")) + + def _progress_get(self, progress_id, profile): + data = self.progress_get(progress_id, profile) + return {k: str(v) for k, v in data.items()} + + def progress_get(self, progress_id, profile): + """Return a dict with progress information + + @param progress_id(unicode): unique id of the progressing element + @param profile: %(doc_profile)s + @return (dict): data with the following keys: + 'position' (int): current possition + 'size' (int): end_position + if id doesn't exists (may be a finished progression), and empty dict is + returned + """ + client = self.get_client(profile) + try: + data = client._progress_cb[progress_id][0](progress_id, profile) + except KeyError: + data = {} + return data + + def _progress_get_all(self, profile_key): + progress_all = self.progress_get_all(profile_key) + for profile, progress_dict in progress_all.items(): + for progress_id, data in progress_dict.items(): + for key, value in data.items(): + data[key] = str(value) + return progress_all + + def progress_get_all_metadata(self, profile_key): + """Return all progress metadata at once + + @param profile_key: %(doc_profile)s + if C.PROF_KEY_ALL is used, all progress metadata from all profiles are + returned + @return (dict[dict[dict]]): a dict which map profile to progress_dict + progress_dict map progress_id to progress_data + progress_metadata is the same dict as sent by [progress_started] + """ + clients = self.get_clients(profile_key) + progress_all = {} + for client in clients: + profile = client.profile + progress_dict = {} + progress_all[profile] = progress_dict + for ( + progress_id, + (__, progress_metadata), + ) in client._progress_cb.items(): + progress_dict[progress_id] = progress_metadata + return progress_all + + def progress_get_all(self, profile_key): + """Return all progress status at once + + @param profile_key: %(doc_profile)s + if C.PROF_KEY_ALL is used, all progress status from all profiles are returned + @return (dict[dict[dict]]): a dict which map profile to progress_dict + progress_dict map progress_id to progress_data + progress_data is the same dict as returned by [progress_get] + """ + clients = self.get_clients(profile_key) + progress_all = {} + for client in clients: + profile = client.profile + progress_dict = {} + progress_all[profile] = progress_dict + for progress_id, (progress_cb, __) in client._progress_cb.items(): + progress_dict[progress_id] = progress_cb(progress_id, profile) + return progress_all + + def register_callback(self, callback, *args, **kwargs): + """Register a callback. + + @param callback(callable): method to call + @param kwargs: can contain: + with_data(bool): True if the callback use the optional data dict + force_id(unicode): id to avoid generated id. Can lead to name conflict, avoid + if possible + one_shot(bool): True to delete callback once it has been called + @return: id of the registered callback + """ + callback_id = kwargs.pop("force_id", None) + if callback_id is None: + callback_id = str(uuid.uuid4()) + else: + if callback_id in self._cb_map: + raise exceptions.ConflictError(_("id already registered")) + self._cb_map[callback_id] = (callback, args, kwargs) + + if "one_shot" in kwargs: # One Shot callback are removed after 30 min + + def purge_callback(): + try: + self.remove_callback(callback_id) + except KeyError: + pass + + reactor.callLater(1800, purge_callback) + + return callback_id + + def remove_callback(self, callback_id): + """ Remove a previously registered callback + @param callback_id: id returned by [register_callback] """ + log.debug("Removing callback [%s]" % callback_id) + del self._cb_map[callback_id] + + def _action_launch( + self, + callback_id: str, + data_s: str, + profile_key: str + ) -> defer.Deferred: + d = self.launch_callback( + callback_id, + data_format.deserialise(data_s), + profile_key + ) + d.addCallback(data_format.serialise) + return d + + def launch_callback( + self, + callback_id: str, + data: Optional[dict] = None, + profile_key: str = C.PROF_KEY_NONE + ) -> defer.Deferred: + """Launch a specific callback + + @param callback_id: id of the action (callback) to launch + @param data: optional data + @profile_key: %(doc_profile_key)s + @return: a deferred which fire a dict where key can be: + - xmlui: a XMLUI need to be displayed + - validated: if present, can be used to launch a callback, it can have the + values + - C.BOOL_TRUE + - C.BOOL_FALSE + """ + # FIXME: is it possible to use this method without profile connected? If not, + # client must be used instead of profile_key + # FIXME: security limit need to be checked here + try: + client = self.get_client(profile_key) + except exceptions.NotFound: + # client is not available yet + profile = self.memory.get_profile_name(profile_key) + if not profile: + raise exceptions.ProfileUnknownError( + _("trying to launch action with a non-existant profile") + ) + else: + profile = client.profile + # we check if the action is kept, and remove it + try: + action_tuple = client.actions[callback_id] + except KeyError: + pass + else: + action_tuple[-1].cancel() # the last item is the action timer + del client.actions[callback_id] + + try: + callback, args, kwargs = self._cb_map[callback_id] + except KeyError: + raise exceptions.DataError("Unknown callback id {}".format(callback_id)) + + if kwargs.get("with_data", False): + if data is None: + raise exceptions.DataError("Required data for this callback is missing") + args, kwargs = ( + list(args)[:], + kwargs.copy(), + ) # we don't want to modify the original (kw)args + args.insert(0, data) + kwargs["profile"] = profile + del kwargs["with_data"] + + if kwargs.pop("one_shot", False): + self.remove_callback(callback_id) + + return utils.as_deferred(callback, *args, **kwargs) + + # Menus management + + def _get_menu_canonical_path(self, path): + """give canonical form of path + + canonical form is a tuple of the path were every element is stripped and lowercase + @param path(iterable[unicode]): untranslated path to menu + @return (tuple[unicode]): canonical form of path + """ + return tuple((p.lower().strip() for p in path)) + + def import_menu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, + help_string="", type_=C.MENU_GLOBAL): + r"""register a new menu for frontends + + @param path(iterable[unicode]): path to go to the menu + (category/subcategory/.../item) (e.g.: ("File", "Open")) + /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open"))) + untranslated/lower case path can be used to identity a menu, for this reason + it must be unique independently of case. + @param callback(callable): method to be called when menuitem is selected, callable + or a callback id (string) as returned by [register_callback] + @param security_limit(int): %(doc_security_limit)s + /!\ security_limit MUST be added to data in launch_callback if used #TODO + @param help_string(unicode): string used to indicate what the menu do (can be + show as a tooltip). + /!\ use D_() instead of _() for translations + @param type(unicode): one of: + - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. + something like File/Open) + - C.MENU_ROOM: like a global menu, but only shown in multi-user chat + menu_data must contain a "room_jid" data + - C.MENU_SINGLE: like a global menu, but only shown in one2one chat + menu_data must contain a "jid" data + - C.MENU_JID_CONTEXT: contextual menu, used with any jid (e.g.: ad hoc + commands, jid is already filled) + menu_data must contain a "jid" data + - C.MENU_ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in + roster. + menu_data must contain a "room_jid" data + - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish + microblog, group is already filled) + menu_data must contain a "group" data + @return (unicode): menu_id (same as callback_id) + """ + + if callable(callback): + callback_id = self.register_callback(callback, with_data=True) + elif isinstance(callback, str): + # The callback is already registered + callback_id = callback + try: + callback, args, kwargs = self._cb_map[callback_id] + except KeyError: + raise exceptions.DataError("Unknown callback id") + kwargs["with_data"] = True # we have to be sure that we use extra data + else: + raise exceptions.DataError("Unknown callback type") + + for menu_data in self._menus.values(): + if menu_data["path"] == path and menu_data["type"] == type_: + raise exceptions.ConflictError( + _("A menu with the same path and type already exists") + ) + + path_canonical = self._get_menu_canonical_path(path) + menu_key = (type_, path_canonical) + + if menu_key in self._menus_paths: + raise exceptions.ConflictError( + "this menu path is already used: {path} ({menu_key})".format( + path=path_canonical, menu_key=menu_key + ) + ) + + menu_data = { + "path": tuple(path), + "path_canonical": path_canonical, + "security_limit": security_limit, + "help_string": help_string, + "type": type_, + } + + self._menus[callback_id] = menu_data + self._menus_paths[menu_key] = callback_id + + return callback_id + + def get_menus(self, language="", security_limit=C.NO_SECURITY_LIMIT): + """Return all menus registered + + @param language: language used for translation, or empty string for default + @param security_limit: %(doc_security_limit)s + @return: array of tuple with: + - menu id (same as callback_id) + - menu type + - raw menu path (array of strings) + - translated menu path + - extra (dict(unicode, unicode)): extra data where key can be: + - icon: name of the icon to use (TODO) + - help_url: link to a page with more complete documentation (TODO) + """ + ret = [] + for menu_id, menu_data in self._menus.items(): + type_ = menu_data["type"] + path = menu_data["path"] + menu_security_limit = menu_data["security_limit"] + if security_limit != C.NO_SECURITY_LIMIT and ( + menu_security_limit == C.NO_SECURITY_LIMIT + or menu_security_limit > security_limit + ): + continue + language_switch(language) + path_i18n = [_(elt) for elt in path] + language_switch() + extra = {} # TODO: manage extra data like icon + ret.append((menu_id, type_, path, path_i18n, extra)) + + return ret + + def _launch_menu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE): + client = self.get_client(profile_key) + return self.launch_menu(client, menu_type, path, data, security_limit) + + def launch_menu(self, client, menu_type, path, data=None, + security_limit=C.NO_SECURITY_LIMIT): + """launch action a menu action + + @param menu_type(unicode): type of menu to launch + @param path(iterable[unicode]): canonical path of the menu + @params data(dict): menu data + @raise NotFound: this path is not known + """ + # FIXME: manage security_limit here + # defaut security limit should be high instead of C.NO_SECURITY_LIMIT + canonical_path = self._get_menu_canonical_path(path) + menu_key = (menu_type, canonical_path) + try: + callback_id = self._menus_paths[menu_key] + except KeyError: + raise exceptions.NotFound( + "Can't find menu {path} ({menu_type})".format( + path=canonical_path, menu_type=menu_type + ) + ) + return self.launch_callback(callback_id, data, client.profile) + + def get_menu_help(self, menu_id, language=""): + """return the help string of the menu + + @param menu_id: id of the menu (same as callback_id) + @param language: language used for translation, or empty string for default + @param return: translated help + + """ + try: + menu_data = self._menus[menu_id] + except KeyError: + raise exceptions.DataError("Trying to access an unknown menu") + language_switch(language) + help_string = _(menu_data["help_string"]) + language_switch() + return help_string diff -r d10748475025 -r 4b842c1fb686 libervia/backend/core/xmpp.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/core/xmpp.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1953 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import calendar +import copy +from functools import partial +import mimetypes +from pathlib import Path +import sys +import time +from typing import Callable, Dict, Tuple, Optional +from urllib.parse import unquote, urlparse +import uuid + +import shortuuid +from twisted.internet import defer, error as internet_error +from twisted.internet import ssl +from twisted.python import failure +from twisted.words.protocols.jabber import xmlstream +from twisted.words.protocols.jabber import error +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.xmlstream import XMPPHandler +from twisted.words.xish import domish +from wokkel import client as wokkel_client, disco, generic, iwokkel, xmppim +from wokkel import component +from wokkel import delay +from zope.interface import implementer + +from libervia.backend.core import exceptions +from libervia.backend.core import core_types +from libervia.backend.core.constants import Const as C +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.memory import cache +from libervia.backend.memory import encryption +from libervia.backend.memory import persistent +from libervia.backend.tools import xml_tools +from libervia.backend.tools import utils +from libervia.backend.tools.common import data_format + +log = getLogger(__name__) + + +NS_X_DATA = "jabber:x:data" +NS_DISCO_INFO = "http://jabber.org/protocol/disco#info" +NS_XML_ELEMENT = "urn:xmpp:xml-element" +NS_ROSTER_VER = "urn:xmpp:features:rosterver" +# we use 2 "@" which is illegal in a jid, to be sure we are not mixing keys +# with roster jids +ROSTER_VER_KEY = "@version@" + + +class ClientPluginWrapper: + """Use a plugin with default value if plugin is missing""" + + def __init__(self, client, plugin_name, missing): + self.client = client + self.plugin = client.host_app.plugins.get(plugin_name) + if self.plugin is None: + self.plugin_name = plugin_name + self.missing = missing + + def __getattr__(self, attr): + if self.plugin is None: + missing = self.missing + if isinstance(missing, type) and issubclass(missing, Exception): + raise missing(f"plugin {self.plugin_name!r} is not available") + elif isinstance(missing, Exception): + raise missing + else: + return lambda *args, **kwargs: missing + return partial(getattr(self.plugin, attr), self.client) + + +class SatXMPPEntity(core_types.SatXMPPEntity): + """Common code for Client and Component""" + # profile is added there when start_connection begins and removed when it is finished + profiles_connecting = set() + + def __init__(self, host_app, profile, max_retries): + factory = self.factory + + # we monkey patch clientConnectionLost to handle network_enabled/network_disabled + # and to allow plugins to tune reconnection mechanism + clientConnectionFailed_ori = factory.clientConnectionFailed + clientConnectionLost_ori = factory.clientConnectionLost + factory.clientConnectionFailed = partial( + self.connection_terminated, term_type="failed", cb=clientConnectionFailed_ori) + factory.clientConnectionLost = partial( + self.connection_terminated, term_type="lost", cb=clientConnectionLost_ori) + + factory.maxRetries = max_retries + factory.maxDelay = 30 + # when self._connected_d is None, we are not connected + # else, it's a deferred which fire on disconnection + self._connected_d = None + self.profile = profile + self.host_app = host_app + self.cache = cache.Cache(host_app, profile) + self.mess_id2uid = {} # map from message id to uid used in history. + # Key: (full_jid, message_id) Value: uid + # this Deferred fire when entity is connected + self.conn_deferred = defer.Deferred() + self._progress_cb = {} # callback called when a progress is requested + # (key = progress id) + self.actions = {} # used to keep track of actions for retrieval (key = action_id) + self.encryption = encryption.EncryptionHandler(self) + + def __str__(self): + return f"Client for profile {self.profile}" + + def __repr__(self): + return f"{super().__repr__()} - profile: {self.profile!r}" + + ## initialisation ## + + async def _call_connection_triggers(self, connection_timer): + """Call conneting trigger prepare connected trigger + + @param plugins(iterable): plugins to use + @return (list[object, callable]): plugin to trigger tuples with: + - plugin instance + - profile_connected* triggers (to call after connection) + """ + plugin_conn_cb = [] + for plugin in self._get_plugins_list(): + # we check if plugin handle client mode + if plugin.is_handler: + plugin.get_handler(self).setHandlerParent(self) + + # profile_connecting/profile_connected methods handling + + timer = connection_timer[plugin] = { + "total": 0 + } + # profile connecting is called right now (before actually starting client) + connecting_cb = getattr(plugin, "profile_connecting", None) + if connecting_cb is not None: + connecting_start = time.time() + await utils.as_deferred(connecting_cb, self) + timer["connecting"] = time.time() - connecting_start + timer["total"] += timer["connecting"] + + # profile connected is called after client is ready and roster is got + connected_cb = getattr(plugin, "profile_connected", None) + if connected_cb is not None: + plugin_conn_cb.append((plugin, connected_cb)) + + return plugin_conn_cb + + def _get_plugins_list(self): + """Return list of plugin to use + + need to be implemented by subclasses + this list is used to call profileConnect* triggers + @return(iterable[object]): plugins to use + """ + raise NotImplementedError + + def _create_sub_protocols(self): + return + + def entity_connected(self): + """Called once connection is done + + may return a Deferred, to perform initialisation tasks + """ + return + + @staticmethod + async def _run_profile_connected( + callback: Callable, + entity: "SatXMPPEntity", + timer: Dict[str, float] + ) -> None: + connected_start = time.time() + await utils.as_deferred(callback, entity) + timer["connected"] = time.time() - connected_start + timer["total"] += timer["connected"] + + @classmethod + async def start_connection(cls, host, profile, max_retries): + """instantiate the entity and start the connection""" + # FIXME: reconnection doesn't seems to be handled correclty + # (client is deleted then recreated from scratch) + # most of methods called here should be called once on first connection + # (e.g. adding subprotocols) + # but client should not be deleted except if session is finished + # (independently of connection/deconnection) + if profile in cls.profiles_connecting: + raise exceptions.CancelError(f"{profile} is already being connected") + cls.profiles_connecting.add(profile) + try: + try: + port = int( + host.memory.param_get_a( + C.FORCE_PORT_PARAM, "Connection", profile_key=profile + ) + ) + except ValueError: + log.debug(_("Can't parse port value, using default value")) + port = ( + None + ) # will use default value 5222 or be retrieved from a DNS SRV record + + password = await host.memory.param_get_a_async( + "Password", "Connection", profile_key=profile + ) + + entity_jid_s = await host.memory.param_get_a_async( + "JabberID", "Connection", profile_key=profile) + entity_jid = jid.JID(entity_jid_s) + + if not entity_jid.resource and not cls.is_component and entity_jid.user: + # if no resource is specified, we create our own instead of using + # server returned one, as it will then stay stable in case of + # reconnection. we only do that for client and if there is a user part, to + # let server decide for anonymous login + resource_dict = await host.memory.storage.get_privates( + "core:xmpp", ["resource"] , profile=profile) + try: + resource = resource_dict["resource"] + except KeyError: + resource = f"{C.APP_NAME_FILE}.{shortuuid.uuid()}" + await host.memory.storage.set_private_value( + "core:xmpp", "resource", resource, profile=profile) + + log.info(_("We'll use the stable resource {resource}").format( + resource=resource)) + entity_jid.resource = resource + + if profile in host.profiles: + if host.profiles[profile].is_connected(): + raise exceptions.InternalError( + f"There is already a connected profile of name {profile!r} in " + f"host") + log.debug( + "removing unconnected profile {profile!r}") + del host.profiles[profile] + entity = host.profiles[profile] = cls( + host, profile, entity_jid, password, + host.memory.param_get_a(C.FORCE_SERVER_PARAM, "Connection", + profile_key=profile) or None, + port, max_retries, + ) + + await entity.encryption.load_sessions() + + entity._create_sub_protocols() + + entity.fallBack = SatFallbackHandler(host) + entity.fallBack.setHandlerParent(entity) + + entity.versionHandler = SatVersionHandler(C.APP_NAME, host.full_version) + entity.versionHandler.setHandlerParent(entity) + + entity.identityHandler = SatIdentityHandler() + entity.identityHandler.setHandlerParent(entity) + + log.debug(_("setting plugins parents")) + + connection_timer: Dict[str, Dict[str, float]] = {} + plugin_conn_cb = await entity._call_connection_triggers(connection_timer) + + entity.startService() + + await entity.conn_deferred + + await defer.maybeDeferred(entity.entity_connected) + + # Call profile_connected callback for all plugins, + # and print error message if any of them fails + conn_cb_list = [] + for plugin, callback in plugin_conn_cb: + conn_cb_list.append( + defer.ensureDeferred( + cls._run_profile_connected( + callback, entity, connection_timer[plugin] + ) + ) + ) + list_d = defer.DeferredList(conn_cb_list) + + def log_plugin_results(results): + if not results: + log.info("no plugin loaded") + return + all_succeed = all([success for success, result in results]) + if not all_succeed: + log.error(_("Plugins initialisation error")) + for idx, (success, result) in enumerate(results): + if not success: + plugin_name = plugin_conn_cb[idx][0]._info["import_name"] + log.error(f"error (plugin {plugin_name}): {result}") + + log.debug(f"Plugin loading time for {profile!r} (longer to shorter):\n") + plugins_by_timer = sorted( + connection_timer, + key=lambda p: connection_timer[p]["total"], + reverse=True + ) + # total is the addition of all connecting and connected, doesn't really + # reflect the real loading time as connected are launched in a + # DeferredList + total_plugins = 0 + # total real sum all connecting (which happen sequentially) and the + # longuest connected (connected happen in parallel, thus the longuest is + # roughly the total time for connected) + total_real = 0 + total_real = max(t.get("connected", 0) for t in connection_timer.values()) + + for plugin in plugins_by_timer: + name = plugin._info["import_name"] + timer = connection_timer[plugin] + total_plugins += timer["total"] + try: + connecting = f"{timer['connecting']:.2f}s" + except KeyError: + connecting = "n/a" + else: + total_real += timer["connecting"] + try: + connected = f"{timer['connected']:.2f}s" + except KeyError: + connected = "n/a" + log.debug( + f" - {name}: total={timer['total']:.2f}s " + f"connecting={connecting} connected={connected}" + ) + log.debug( + f" Plugins total={total_plugins:.2f}s real={total_real:.2f}s\n" + ) + + await list_d.addCallback( + log_plugin_results + ) # FIXME: we should have a timeout here, and a way to know if a plugin freeze + # TODO: mesure launch time of each plugin + finally: + cls.profiles_connecting.remove(profile) + + def _disconnection_cb(self, __): + self._connected_d = None + + def _disconnection_eb(self, failure_): + log.error(_("Error while disconnecting: {}".format(failure_))) + + def _authd(self, xmlstream): + super(SatXMPPEntity, self)._authd(xmlstream) + log.debug(_("{profile} identified").format(profile=self.profile)) + self.stream_initialized() + + def _finish_connection(self, __): + if self.conn_deferred.called: + # can happen in case of forced disconnection by server + log.debug(f"{self} has already been connected") + else: + self.conn_deferred.callback(None) + + def stream_initialized(self): + """Called after _authd""" + log.debug(_("XML stream is initialized")) + if not self.host_app.trigger.point("xml_init", self): + return + self.post_stream_init() + + def post_stream_init(self): + """Workflow after stream initalisation.""" + log.info( + _("********** [{profile}] CONNECTED **********").format(profile=self.profile) + ) + + # the following Deferred is used to know when we are connected + # so we need to be set it to None when connection is lost + self._connected_d = defer.Deferred() + self._connected_d.addCallback(self._clean_connection) + self._connected_d.addCallback(self._disconnection_cb) + self._connected_d.addErrback(self._disconnection_eb) + + # we send the signal to the clients + self.host_app.bridge.connected(self.jid.full(), self.profile) + + self.disco = SatDiscoProtocol(self) + self.disco.setHandlerParent(self) + self.discoHandler = disco.DiscoHandler() + self.discoHandler.setHandlerParent(self) + disco_d = defer.succeed(None) + + if not self.host_app.trigger.point("Disco handled", disco_d, self.profile): + return + + disco_d.addCallback(self._finish_connection) + + def initializationFailed(self, reason): + log.error( + _( + "ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" + % {"profile": self.profile, "reason": reason} + ) + ) + self.conn_deferred.errback(reason.value) + try: + super(SatXMPPEntity, self).initializationFailed(reason) + except: + # we already chained an errback, no need to raise an exception + pass + + ## connection ## + + def connection_terminated(self, connector, reason, term_type, cb): + """Display disconnection reason, and call factory method + + This method is monkey patched to factory, allowing plugins to handle finely + reconnection with the triggers. + @param connector(twisted.internet.base.BaseConnector): current connector + @param reason(failure.Failure): why connection has been terminated + @param term_type(unicode): on of 'failed' or 'lost' + @param cb(callable): original factory method + + @trigger connection_failed(connector, reason): connection can't be established + @trigger connection_lost(connector, reason): connection was available but it not + anymore + """ + # we save connector because it may be deleted when connection will be dropped + # if reconnection is disabled + self._saved_connector = connector + if reason is not None and not isinstance(reason.value, + internet_error.ConnectionDone): + try: + reason_str = str(reason.value) + except Exception: + # FIXME: workaround for Android were p4a strips docstrings + # while Twisted use docstring in __str__ + # TODO: create a ticket upstream, Twisted should work when optimization + # is used + reason_str = str(reason.value.__class__) + log.warning(f"[{self.profile}] Connection {term_type}: {reason_str}") + if not self.host_app.trigger.point("connection_" + term_type, connector, reason): + return + return cb(connector, reason) + + def network_disabled(self): + """Indicate that network has been completely disabled + + In other words, internet is not available anymore and transport must be stopped. + Retrying is disabled too, as it makes no sense to try without network, and it may + use resources (notably battery on mobiles). + """ + log.info(_("stopping connection because of network disabled")) + self.factory.continueTrying = 0 + self._network_disabled = True + if self.xmlstream is not None: + self.xmlstream.transport.abortConnection() + + def network_enabled(self): + """Indicate that network has been (re)enabled + + This happens when e.g. user activate WIFI connection. + """ + try: + connector = self._saved_connector + network_disabled = self._network_disabled + except AttributeError: + # connection has not been stopped by network_disabled + # we don't have to restart it + log.debug(f"no connection to restart [{self.profile}]") + return + else: + del self._network_disabled + if not network_disabled: + raise exceptions.InternalError("network_disabled should be True") + log.info(_("network is available, trying to connect")) + # we want to be sure to start fresh + self.factory.resetDelay() + # we have a saved connector, meaning the connection has been stopped previously + # we can now try to reconnect + connector.connect() + + def _connected(self, xs): + send_hooks = [] + receive_hooks = [] + self.host_app.trigger.point( + "stream_hooks", self, receive_hooks, send_hooks) + for hook in receive_hooks: + xs.add_hook(C.STREAM_HOOK_RECEIVE, hook) + for hook in send_hooks: + xs.add_hook(C.STREAM_HOOK_SEND, hook) + super(SatXMPPEntity, self)._connected(xs) + + def disconnect_profile(self, reason): + if self._connected_d is not None: + self.host_app.bridge.disconnected( + self.profile + ) # we send the signal to the clients + log.info( + _("********** [{profile}] DISCONNECTED **********").format( + profile=self.profile + ) + ) + # we purge only if no new connection attempt is expected + if not self.factory.continueTrying: + log.debug("continueTrying not set, purging entity") + self._connected_d.callback(None) + # and we remove references to this client + self.host_app.purge_entity(self.profile) + + if not self.conn_deferred.called: + if reason is None: + err = error.StreamError("Server unexpectedly closed the connection") + else: + err = reason + try: + if err.value.args[0][0][2] == "certificate verify failed": + err = exceptions.InvalidCertificate( + _("Your server certificate is not valid " + "(its identity can't be checked).\n\n" + "This should never happen and may indicate that " + "somebody is trying to spy on you.\n" + "Please contact your server administrator.")) + self.factory.stopTrying() + try: + # with invalid certificate, we should not retry to connect + # so we delete saved connector to avoid reconnection if + # network_enabled is called. + del self._saved_connector + except AttributeError: + pass + except (IndexError, TypeError): + pass + self.conn_deferred.errback(err) + + def _disconnected(self, reason): + super(SatXMPPEntity, self)._disconnected(reason) + if not self.host_app.trigger.point("disconnected", self, reason): + return + self.disconnect_profile(reason) + + @defer.inlineCallbacks + def _clean_connection(self, __): + """method called on disconnection + + used to call profile_disconnected* triggers + """ + trigger_name = "profile_disconnected" + for plugin in self._get_plugins_list(): + disconnected_cb = getattr(plugin, trigger_name, None) + if disconnected_cb is not None: + yield disconnected_cb(self) + + def is_connected(self): + """Return True is client is fully connected + + client is considered fully connected if transport is started and all plugins + are initialised + """ + try: + transport_connected = bool(self.xmlstream.transport.connected) + except AttributeError: + return False + + return self._connected_d is not None and transport_connected + + def entity_disconnect(self): + if not self.host_app.trigger.point("disconnecting", self): + return + log.info(_("Disconnecting...")) + self.stopService() + if self._connected_d is not None: + return self._connected_d + else: + return defer.succeed(None) + + ## sending ## + + def IQ(self, type_="set", timeout=60): + """shortcut to create an IQ element managing deferred + + @param type_(unicode): IQ type ('set' or 'get') + @param timeout(None, int): timeout in seconds + @return((D)domish.Element: result stanza + errback is called if an error stanza is returned + """ + iq_elt = xmlstream.IQ(self.xmlstream, type_) + iq_elt.timeout = timeout + return iq_elt + + def sendError(self, iq_elt, condition, text=None, appCondition=None): + """Send error stanza build from iq_elt + + @param iq_elt(domish.Element): initial IQ element + @param condition(unicode): error condition + """ + iq_error_elt = error.StanzaError( + condition, text=text, appCondition=appCondition + ).toResponse(iq_elt) + self.xmlstream.send(iq_error_elt) + + def generate_message_xml( + self, + data: core_types.MessageData, + post_xml_treatments: Optional[defer.Deferred] = None + ) -> core_types.MessageData: + """Generate stanza from message data + + @param data: message data + domish element will be put in data['xml'] + following keys are needed: + - from + - to + - uid: can be set to '' if uid attribute is not wanted + - message + - type + - subject + - extra + @param post_xml_treatments: a Deferred which will be called with data once XML is + generated + @return: message data + """ + data["xml"] = message_elt = domish.Element((None, "message")) + message_elt["to"] = data["to"].full() + message_elt["from"] = data["from"].full() + message_elt["type"] = data["type"] + if data["uid"]: # key must be present but can be set to '' + # by a plugin to avoid id on purpose + message_elt["id"] = data["uid"] + for lang, subject in data["subject"].items(): + subject_elt = message_elt.addElement("subject", content=subject) + if lang: + subject_elt[(C.NS_XML, "lang")] = lang + for lang, message in data["message"].items(): + body_elt = message_elt.addElement("body", content=message) + if lang: + body_elt[(C.NS_XML, "lang")] = lang + try: + thread = data["extra"]["thread"] + except KeyError: + if "thread_parent" in data["extra"]: + raise exceptions.InternalError( + "thread_parent found while there is not associated thread" + ) + else: + thread_elt = message_elt.addElement("thread", content=thread) + try: + thread_elt["parent"] = data["extra"]["thread_parent"] + except KeyError: + pass + + if post_xml_treatments is not None: + post_xml_treatments.callback(data) + return data + + @property + def is_admin(self) -> bool: + """True if a client is an administrator with extra privileges""" + return self.host_app.memory.is_admin(self.profile) + + def add_post_xml_callbacks(self, post_xml_treatments): + """Used to add class level callbacks at the end of the workflow + + @param post_xml_treatments(D): the same Deferred as in sendMessage trigger + """ + raise NotImplementedError + + async def a_send(self, obj): + # original send method accept string + # but we restrict to domish.Element to make trigger treatments easier + assert isinstance(obj, domish.Element) + # XXX: this trigger is the last one before sending stanza on wire + # it is intended for things like end 2 end encryption. + # *DO NOT* cancel (i.e. return False) without very good reason + # (out of band transmission for instance). + # e2e should have a priority of 0 here, and out of band transmission + # a lower priority + if not (await self.host_app.trigger.async_point("send", self, obj)): + return + super().send(obj) + + def send(self, obj): + defer.ensureDeferred(self.a_send(obj)) + + async def send_message_data(self, mess_data): + """Convenient method to send message data to stream + + This method will send mess_data[u'xml'] to stream, but a trigger is there + The trigger can't be cancelled, it's a good place for e2e encryption which + don't handle full stanza encryption + This trigger can return a Deferred (it's an async_point) + @param mess_data(dict): message data as constructed by onMessage workflow + @return (dict): mess_data (so it can be used in a deferred chain) + """ + # XXX: This is the last trigger before u"send" (last but one globally) + # for sending message. + # This is intented for e2e encryption which doesn't do full stanza + # encryption (e.g. OTR) + # This trigger point can't cancel the method + await self.host_app.trigger.async_point("send_message_data", self, mess_data, + triggers_no_cancel=True) + await self.a_send(mess_data["xml"]) + return mess_data + + def sendMessage( + self, to_jid, message, subject=None, mess_type="auto", extra=None, uid=None, + no_trigger=False): + r"""Send a message to an entity + + @param to_jid(jid.JID): destinee of the message + @param message(dict): message body, key is the language (use '' when unknown) + @param subject(dict): message subject, key is the language (use '' when unknown) + @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or: + - auto: for automatic type detection + - info: for information ("info_type" can be specified in extra) + @param extra(dict, None): extra data. Key can be: + - info_type: information type, can be + TODO + @param uid(unicode, None): unique id: + should be unique at least in this XMPP session + if None, an uuid will be generated + @param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used + useful when a message need to be sent without any modification + /!\ this will also skip encryption methods! + """ + if subject is None: + subject = {} + if extra is None: + extra = {} + + assert mess_type in C.MESS_TYPE_ALL + + data = { # dict is similar to the one used in client.onMessage + "from": self.jid, + "to": to_jid, + "uid": uid or str(uuid.uuid4()), + "message": message, + "subject": subject, + "type": mess_type, + "extra": extra, + "timestamp": time.time(), + } + # XXX: plugin can add their pre XML treatments to this deferred + pre_xml_treatments = defer.Deferred() + # XXX: plugin can add their post XML treatments to this deferred + post_xml_treatments = defer.Deferred() + + if data["type"] == C.MESS_TYPE_AUTO: + # we try to guess the type + if data["subject"]: + data["type"] = C.MESS_TYPE_NORMAL + elif not data["to"].resource: + # we may have a groupchat message, we check if the we know this jid + try: + entity_type = self.host_app.memory.get_entity_datum( + self, data["to"], C.ENTITY_TYPE + ) + # FIXME: should entity_type manage resources ? + except (exceptions.UnknownEntityError, KeyError): + entity_type = "contact" + + if entity_type == C.ENTITY_TYPE_MUC: + data["type"] = C.MESS_TYPE_GROUPCHAT + else: + data["type"] = C.MESS_TYPE_CHAT + else: + data["type"] = C.MESS_TYPE_CHAT + + # FIXME: send_only is used by libervia's OTR plugin to avoid + # the triggers from frontend, and no_trigger do the same + # thing internally, this could be unified + send_only = data["extra"].get("send_only", False) + + if not no_trigger and not send_only: + # is the session encrypted? If so we indicate it in data + self.encryption.set_encryption_flag(data) + + if not self.host_app.trigger.point( + "sendMessage" + self.trigger_suffix, + self, + data, + pre_xml_treatments, + post_xml_treatments, + ): + return defer.succeed(None) + + log.debug(_("Sending message (type {type}, to {to})") + .format(type=data["type"], to=to_jid.full())) + + pre_xml_treatments.addCallback(lambda __: self.generate_message_xml(data, post_xml_treatments)) + pre_xml_treatments.addCallback(lambda __: post_xml_treatments) + pre_xml_treatments.addErrback(self._cancel_error_trap) + post_xml_treatments.addCallback( + lambda __: defer.ensureDeferred(self.send_message_data(data)) + ) + if send_only: + log.debug(_("Triggers, storage and echo have been inhibited by the " + "'send_only' parameter")) + else: + self.add_post_xml_callbacks(post_xml_treatments) + post_xml_treatments.addErrback(self._cancel_error_trap) + post_xml_treatments.addErrback(self.host_app.log_errback) + pre_xml_treatments.callback(data) + return pre_xml_treatments + + def _cancel_error_trap(self, failure): + """A message sending can be cancelled by a plugin treatment""" + failure.trap(exceptions.CancelError) + + def is_message_printable(self, mess_data): + """Return True if a message contain payload to show in frontends""" + return ( + mess_data["message"] or mess_data["subject"] + or mess_data["extra"].get(C.KEY_ATTACHMENTS) + or mess_data["type"] == C.MESS_TYPE_INFO + ) + + async def message_add_to_history(self, data): + """Store message into database (for local history) + + @param data: message data dictionnary + @param client: profile's client + """ + if data["type"] != C.MESS_TYPE_GROUPCHAT: + # we don't add groupchat message to history, as we get them back + # and they will be added then + + # we need a message to store + if self.is_message_printable(data): + await self.host_app.memory.add_to_history(self, data) + else: + log.warning( + "No message found" + ) # empty body should be managed by plugins before this point + return data + + def message_get_bridge_args(self, data): + """Generate args to use with bridge from data dict""" + return (data["uid"], data["timestamp"], data["from"].full(), + data["to"].full(), data["message"], data["subject"], + data["type"], data_format.serialise(data["extra"])) + + + def message_send_to_bridge(self, data): + """Send message to bridge, so frontends can display it + + @param data: message data dictionnary + @param client: profile's client + """ + if data["type"] != C.MESS_TYPE_GROUPCHAT: + # we don't send groupchat message to bridge, as we get them back + # and they will be added the + + # we need a message to send something + if self.is_message_printable(data): + + # We send back the message, so all frontends are aware of it + self.host_app.bridge.message_new( + *self.message_get_bridge_args(data), + profile=self.profile + ) + else: + log.warning(_("No message found")) + return data + + ## helper methods ## + + def p(self, plugin_name, missing=exceptions.MissingModule): + """Get a plugin if available + + @param plugin_name(str): name of the plugin + @param missing(object): value to return if plugin is missing + if it is a subclass of Exception, it will be raised with a helping str as + argument. + @return (object): requested plugin wrapper, or default value + The plugin wrapper will return the method with client set as first + positional argument + """ + return ClientPluginWrapper(self, plugin_name, missing) + + +ExtraDict = dict # TODO + + +@implementer(iwokkel.IDisco) +class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient): + trigger_suffix = "" + is_component = False + + def __init__(self, host_app, profile, user_jid, password, host=None, + port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES): + # XXX: DNS SRV records are checked when the host is not specified. + # If no SRV record is found, the host is directly extracted from the JID. + self.started = time.time() + + # Currently, we use "client/pc/Salut à Toi", but as + # SàT is multi-frontends and can be used on mobile devices, as a bot, + # with a web frontend, + # etc., we should implement a way to dynamically update identities through the + # bridge + self.identities = [disco.DiscoIdentity("client", "pc", C.APP_NAME)] + if sys.platform == "android": + # for now we consider Android devices to be always phones + self.identities = [disco.DiscoIdentity("client", "phone", C.APP_NAME)] + + hosts_map = host_app.memory.config_get(None, "hosts_dict", {}) + if host is None and user_jid.host in hosts_map: + host_data = hosts_map[user_jid.host] + if isinstance(host_data, str): + host = host_data + elif isinstance(host_data, dict): + if "host" in host_data: + host = host_data["host"] + if "port" in host_data: + port = host_data["port"] + else: + log.warning( + _("invalid data used for host: {data}").format(data=host_data) + ) + host_data = None + if host_data is not None: + log.info( + "using {host}:{port} for host {host_ori} as requested in config" + .format(host_ori=user_jid.host, host=host, port=port) + ) + + self.check_certificate = host_app.memory.param_get_a( + "check_certificate", "Connection", profile_key=profile) + + if self.check_certificate: + tls_required, configurationForTLS = True, None + else: + tls_required = False + configurationForTLS = ssl.CertificateOptions(trustRoot=None) + + wokkel_client.XMPPClient.__init__( + self, user_jid, password, host or None, port or C.XMPP_C2S_PORT, + tls_required=tls_required, configurationForTLS=configurationForTLS + ) + SatXMPPEntity.__init__(self, host_app, profile, max_retries) + + if not self.check_certificate: + msg = (_("Certificate validation is deactivated, this is unsecure and " + "somebody may be spying on you. If you have no good reason to disable " + "certificate validation, please activate \"Check certificate\" in your " + "settings in \"Connection\" tab.")) + xml_tools.quick_note(host_app, self, msg, _("Security notice"), + level = C.XMLUI_DATA_LVL_WARNING) + + @property + def server_jid(self): + return jid.JID(self.jid.host) + + def _get_plugins_list(self): + for p in self.host_app.plugins.values(): + if C.PLUG_MODE_CLIENT in p._info["modes"]: + yield p + + def _create_sub_protocols(self): + self.messageProt = SatMessageProtocol(self.host_app) + self.messageProt.setHandlerParent(self) + + self.roster = SatRosterProtocol(self.host_app) + self.roster.setHandlerParent(self) + + self.presence = SatPresenceProtocol(self.host_app) + self.presence.setHandlerParent(self) + + @classmethod + async def start_connection(cls, host, profile, max_retries): + try: + await super(SatXMPPClient, cls).start_connection(host, profile, max_retries) + except exceptions.CancelError as e: + log.warning(f"start_connection cancelled: {e}") + return + entity = host.profiles[profile] + # we finally send our presence + entity.presence.available() + + def entity_connected(self): + # we want to be sure that we got the roster + return self.roster.got_roster + + def add_post_xml_callbacks(self, post_xml_treatments): + post_xml_treatments.addCallback(self.messageProt.complete_attachments) + post_xml_treatments.addCallback( + lambda ret: defer.ensureDeferred(self.message_add_to_history(ret)) + ) + post_xml_treatments.addCallback(self.message_send_to_bridge) + + def feedback( + self, + to_jid: jid.JID, + message: str, + extra: Optional[ExtraDict] = None + ) -> None: + """Send message to frontends + + This message will be an info message, not recorded in history. + It can be used to give feedback of a command + @param to_jid: destinee jid + @param message: message to send to frontends + @param extra: extra data to use in particular, info subtype can be specified with + MESS_EXTRA_INFO + """ + if extra is None: + extra = {} + self.host_app.bridge.message_new( + uid=str(uuid.uuid4()), + timestamp=time.time(), + from_jid=self.jid.full(), + to_jid=to_jid.full(), + message={"": message}, + subject={}, + mess_type=C.MESS_TYPE_INFO, + extra=data_format.serialise(extra), + profile=self.profile, + ) + + def _finish_connection(self, __): + d = self.roster.request_roster() + d.addCallback(lambda __: super(SatXMPPClient, self)._finish_connection(__)) + + +@implementer(iwokkel.IDisco) +class SatXMPPComponent(SatXMPPEntity, component.Component): + """XMPP component + + This component are similar but not identical to clients. + An entry point plugin is launched after component is connected. + Component need to instantiate MessageProtocol itself + """ + + trigger_suffix = ( + "Component" + ) # used for to distinguish some trigger points set in SatXMPPEntity + is_component = True + # XXX: set to True from entry plugin to keep messages in history for sent messages + sendHistory = False + # XXX: same as sendHistory but for received messaged + receiveHistory = False + + def __init__(self, host_app, profile, component_jid, password, host=None, port=None, + max_retries=C.XMPP_MAX_RETRIES): + self.started = time.time() + if port is None: + port = C.XMPP_COMPONENT_PORT + + ## entry point ## + entry_point = host_app.memory.get_entry_point(profile) + try: + self.entry_plugin = host_app.plugins[entry_point] + except KeyError: + raise exceptions.NotFound( + _("The requested entry point ({entry_point}) is not available").format( + entry_point=entry_point + ) + ) + + self.enabled_features = set() + self.identities = [disco.DiscoIdentity("component", "generic", C.APP_NAME)] + # jid is set automatically on bind by Twisted for Client, but not for Component + self.jid = component_jid + if host is None: + try: + host = component_jid.host.split(".", 1)[1] + except IndexError: + raise ValueError("Can't guess host from jid, please specify a host") + # XXX: component.Component expect unicode jid, while Client expect jid.JID. + # this is not consistent, so we use jid.JID for SatXMPP* + component.Component.__init__(self, host, port, component_jid.full(), password) + SatXMPPEntity.__init__(self, host_app, profile, max_retries) + + @property + def server_jid(self): + # FIXME: not the best way to get server jid, maybe use config option? + return jid.JID(self.jid.host.split(".", 1)[-1]) + + @property + def is_admin(self) -> bool: + return False + + def _create_sub_protocols(self): + self.messageProt = SatMessageProtocol(self.host_app) + self.messageProt.setHandlerParent(self) + + def _build_dependencies(self, current, plugins, required=True): + """build recursively dependencies needed for a plugin + + this method build list of plugin needed for a component and raises + errors if they are not available or not allowed for components + @param current(object): parent plugin to check + use entry_point for first call + @param plugins(list): list of validated plugins, will be filled by the method + give an empty list for first call + @param required(bool): True if plugin is mandatory + for recursive calls only, should not be modified by inital caller + @raise InternalError: one of the plugin is not handling components + @raise KeyError: one plugin should be present in self.host_app.plugins but it + is not + """ + if C.PLUG_MODE_COMPONENT not in current._info["modes"]: + if not required: + return + else: + log.error( + _( + "Plugin {current_name} is needed for {entry_name}, " + "but it doesn't handle component mode" + ).format( + current_name=current._info["import_name"], + entry_name=self.entry_plugin._info["import_name"], + ) + ) + raise exceptions.InternalError(_("invalid plugin mode")) + + for import_name in current._info.get(C.PI_DEPENDENCIES, []): + # plugins are already loaded as dependencies + # so we know they are in self.host_app.plugins + dep = self.host_app.plugins[import_name] + self._build_dependencies(dep, plugins) + + for import_name in current._info.get(C.PI_RECOMMENDATIONS, []): + # here plugins are only recommendations, + # so they may not exist in self.host_app.plugins + try: + dep = self.host_app.plugins[import_name] + except KeyError: + continue + self._build_dependencies(dep, plugins, required=False) + + if current not in plugins: + # current can be required for several plugins and so + # it can already be present in the list + plugins.append(current) + + def _get_plugins_list(self): + # XXX: for component we don't launch all plugins triggers + # but only the ones from which there is a dependency + plugins = [] + self._build_dependencies(self.entry_plugin, plugins) + return plugins + + def entity_connected(self): + # we can now launch entry point + try: + start_cb = self.entry_plugin.componentStart + except AttributeError: + return + else: + return start_cb(self) + + def add_post_xml_callbacks(self, post_xml_treatments): + if self.sendHistory: + post_xml_treatments.addCallback( + lambda ret: defer.ensureDeferred(self.message_add_to_history(ret)) + ) + + def get_owner_from_jid(self, to_jid: jid.JID) -> jid.JID: + """Retrieve "owner" of a component resource from the destination jid of the request + + This method needs plugin XEP-0106 for unescaping, if you use it you must add the + plugin to your dependencies. + A "user" part must be present in "to_jid" (otherwise, the component itself is addressed) + @param to_jid: destination JID of the request + """ + try: + unescape = self.host_app.plugins['XEP-0106'].unescape + except KeyError: + raise exceptions.MissingPlugin("Plugin XEP-0106 is needed to retrieve owner") + else: + user = unescape(to_jid.user) + if '@' in user: + # a full jid is specified + return jid.JID(user) + else: + # only user part is specified, we use our own host to build the full jid + return jid.JID(None, (user, self.host, None)) + + def get_owner_and_peer(self, iq_elt: domish.Element) -> Tuple[jid.JID, jid.JID]: + """Retrieve owner of a component jid, and the jid of the requesting peer + + "owner" is found by either unescaping full jid from node, or by combining node + with our host. + Peer jid is the requesting jid from the IQ element + @param iq_elt: IQ stanza sent from the requested + @return: owner and peer JIDs + """ + to_jid = jid.JID(iq_elt['to']) + if to_jid.user: + owner = self.get_owner_from_jid(to_jid) + else: + owner = jid.JID(iq_elt["from"]).userhostJID() + + peer_jid = jid.JID(iq_elt["from"]) + return peer_jid, owner + + def get_virtual_client(self, jid_: jid.JID) -> SatXMPPEntity: + """Get client for this component with a specified jid + + This is needed to perform operations with a virtual JID corresponding to a virtual + entity (e.g. identified of a legacy network account) instead of the JID of the + gateway itself. + @param jid_: virtual JID to use + @return: virtual client + """ + client = copy.copy(self) + client.jid = jid_ + return client + + +class SatMessageProtocol(xmppim.MessageProtocol): + + def __init__(self, host): + xmppim.MessageProtocol.__init__(self) + self.host = host + + @property + def client(self): + return self.parent + + def normalize_ns(self, elt: domish.Element, namespace: Optional[str]) -> None: + if elt.uri == namespace: + elt.defaultUri = elt.uri = C.NS_CLIENT + for child in elt.elements(): + self.normalize_ns(child, namespace) + + def parse_message(self, message_elt): + """Parse a message XML and return message_data + + @param message_elt(domish.Element): raw xml + @param client(SatXMPPClient, None): client to map message id to uid + if None, mapping will not be done + @return(dict): message data + """ + if message_elt.name != "message": + log.warning(_( + "parse_message used with a non stanza, ignoring: {xml}" + .format(xml=message_elt.toXml()))) + return {} + + if message_elt.uri == None: + # xmlns may be None when wokkel element parsing strip out root namespace + self.normalize_ns(message_elt, None) + elif message_elt.uri != C.NS_CLIENT: + log.warning(_( + "received with a wrong namespace: {xml}" + .format(xml=message_elt.toXml()))) + + client = self.parent + + if not message_elt.hasAttribute('to'): + message_elt['to'] = client.jid.full() + + message = {} + subject = {} + extra = {} + data = { + "from": jid.JID(message_elt["from"]), + "to": jid.JID(message_elt["to"]), + "uid": message_elt.getAttribute( + "uid", str(uuid.uuid4()) + ), # XXX: uid is not a standard attribute but may be added by plugins + "message": message, + "subject": subject, + "type": message_elt.getAttribute("type", "normal"), + "extra": extra, + } + + try: + message_id = data["extra"]["message_id"] = message_elt["id"] + except KeyError: + pass + else: + client.mess_id2uid[(data["from"], message_id)] = data["uid"] + + # message + for e in message_elt.elements(C.NS_CLIENT, "body"): + message[e.getAttribute((C.NS_XML, "lang"), "")] = str(e) + + # subject + for e in message_elt.elements(C.NS_CLIENT, "subject"): + subject[e.getAttribute((C.NS_XML, "lang"), "")] = str(e) + + # delay and timestamp + try: + received_timestamp = message_elt._received_timestamp + except AttributeError: + # message_elt._received_timestamp should have been set in onMessage + # but if parse_message is called directly, it can be missing + log.debug("missing received timestamp for {message_elt}".format( + message_elt=message_elt)) + received_timestamp = time.time() + + try: + delay_elt = next(message_elt.elements(delay.NS_DELAY, "delay")) + except StopIteration: + data["timestamp"] = received_timestamp + else: + parsed_delay = delay.Delay.fromElement(delay_elt) + data["timestamp"] = calendar.timegm(parsed_delay.stamp.utctimetuple()) + data["received_timestamp"] = received_timestamp + if parsed_delay.sender: + data["delay_sender"] = parsed_delay.sender.full() + + self.host.trigger.point("message_parse", client, message_elt, data) + return data + + def _on_message_start_workflow(self, cont, client, message_elt, post_treat): + """Parse message and do post treatments + + It is the first callback called after message_received trigger + @param cont(bool): workflow will continue only if this is True + @param message_elt(domish.Element): message stanza + may have be modified by triggers + @param post_treat(defer.Deferred): post parsing treatments + """ + if not cont: + return + data = self.parse_message(message_elt) + post_treat.addCallback(self.complete_attachments) + post_treat.addCallback(self.skip_empty_message) + if not client.is_component or client.receiveHistory: + post_treat.addCallback( + lambda ret: defer.ensureDeferred(self.add_to_history(ret)) + ) + if not client.is_component: + post_treat.addCallback(self.bridge_signal, data) + post_treat.addErrback(self.cancel_error_trap) + post_treat.callback(data) + + def onMessage(self, message_elt): + # TODO: handle threads + message_elt._received_timestamp = time.time() + client = self.parent + if not "from" in message_elt.attributes: + message_elt["from"] = client.jid.host + log.debug(_("got message from: {from_}").format(from_=message_elt["from"])) + if self.client.is_component and message_elt.uri == component.NS_COMPONENT_ACCEPT: + # we use client namespace all the time to simplify parsing + self.normalize_ns(message_elt, component.NS_COMPONENT_ACCEPT) + + # plugin can add their treatments to this deferred + post_treat = defer.Deferred() + + d = self.host.trigger.async_point( + "message_received", client, message_elt, post_treat + ) + + d.addCallback(self._on_message_start_workflow, client, message_elt, post_treat) + + def complete_attachments(self, data): + """Complete missing metadata of attachments""" + for attachment in data['extra'].get(C.KEY_ATTACHMENTS, []): + if "name" not in attachment and "url" in attachment: + name = (Path(unquote(urlparse(attachment['url']).path)).name + or C.FILE_DEFAULT_NAME) + attachment["name"] = name + if ((C.KEY_ATTACHMENTS_MEDIA_TYPE not in attachment + and "name" in attachment)): + media_type = mimetypes.guess_type(attachment['name'], strict=False)[0] + if media_type: + attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type + + return data + + def skip_empty_message(self, data): + if not data["message"] and not data["extra"] and not data["subject"]: + raise failure.Failure(exceptions.CancelError("Cancelled empty message")) + return data + + async def add_to_history(self, data): + if data.pop("history", None) == C.HISTORY_SKIP: + log.debug("history is skipped as requested") + data["extra"]["history"] = C.HISTORY_SKIP + else: + # we need a message to store + if self.parent.is_message_printable(data): + return await self.host.memory.add_to_history(self.parent, data) + else: + log.debug("not storing empty message to history: {data}" + .format(data=data)) + + def bridge_signal(self, __, data): + try: + data["extra"]["received_timestamp"] = str(data["received_timestamp"]) + data["extra"]["delay_sender"] = data["delay_sender"] + except KeyError: + pass + if self.client.encryption.isEncrypted(data): + data["extra"]["encrypted"] = True + if data is not None: + if self.parent.is_message_printable(data): + self.host.bridge.message_new( + data["uid"], + data["timestamp"], + data["from"].full(), + data["to"].full(), + data["message"], + data["subject"], + data["type"], + data_format.serialise(data["extra"]), + profile=self.parent.profile, + ) + else: + log.debug("Discarding bridge signal for empty message: {data}".format( + data=data)) + return data + + def cancel_error_trap(self, failure_): + """A message sending can be cancelled by a plugin treatment""" + failure_.trap(exceptions.CancelError) + + +class SatRosterProtocol(xmppim.RosterClientProtocol): + + def __init__(self, host): + xmppim.RosterClientProtocol.__init__(self) + self.host = host + self.got_roster = defer.Deferred() # called when roster is received and ready + # XXX: the two following dicts keep a local copy of the roster + self._jids = {} # map from jids to RosterItem: key=jid value=RosterItem + self._groups = {} # map from groups to jids: key=group value=set of jids + + def __contains__(self, entity_jid): + return self.is_jid_in_roster(entity_jid) + + @property + def versioning(self): + """True if server support roster versioning""" + return (NS_ROSTER_VER, 'ver') in self.parent.xmlstream.features + + @property + def roster_cache(self): + """Cache of roster from storage + + This property return a new PersistentDict on each call, it must be loaded + manually if necessary + """ + return persistent.PersistentDict(NS_ROSTER_VER, self.parent.profile) + + def _register_item(self, item): + """Register item in local cache + + item must be already registered in self._jids before this method is called + @param item (RosterIem): item added + """ + log.debug("registering item: {}".format(item.entity.full())) + if item.entity.resource: + log.warning( + "Received a roster item with a resource, this is not common but not " + "restricted by RFC 6121, this case may be not well tested." + ) + if not item.subscriptionTo: + if not item.subscriptionFrom: + log.info( + _("There's no subscription between you and [{}]!").format( + item.entity.full() + ) + ) + else: + log.info(_("You are not subscribed to [{}]!").format(item.entity.full())) + if not item.subscriptionFrom: + log.info(_("[{}] is not subscribed to you!").format(item.entity.full())) + + for group in item.groups: + self._groups.setdefault(group, set()).add(item.entity) + + @defer.inlineCallbacks + def _cache_roster(self, version): + """Serialise local roster and save it to storage + + @param version(unicode): version of roster in local cache + """ + roster_cache = self.roster_cache + yield roster_cache.clear() + roster_cache[ROSTER_VER_KEY] = version + for roster_jid, roster_item in self._jids.items(): + roster_jid_s = roster_jid.full() + roster_item_elt = roster_item.toElement().toXml() + roster_cache[roster_jid_s] = roster_item_elt + + @defer.inlineCallbacks + def resync(self): + """Ask full roster to resync database + + this should not be necessary, but may be used if user suspsect roster + to be somehow corrupted + """ + roster_cache = self.roster_cache + yield roster_cache.clear() + self._jids.clear() + self._groups.clear() + yield self.request_roster() + + @defer.inlineCallbacks + def request_roster(self): + """Ask the server for Roster list """ + if self.versioning: + log.info(_("our server support roster versioning, we use it")) + roster_cache = self.roster_cache + yield roster_cache.load() + try: + version = roster_cache[ROSTER_VER_KEY] + except KeyError: + log.info(_("no roster in cache, we start fresh")) + # u"" means we use versioning without valid roster in cache + version = "" + else: + log.info(_("We have roster v{version} in cache").format(version=version)) + # we deserialise cached roster to our local cache + for roster_jid_s, roster_item_elt_s in roster_cache.items(): + if roster_jid_s == ROSTER_VER_KEY: + continue + roster_jid = jid.JID(roster_jid_s) + roster_item_elt = generic.parseXml(roster_item_elt_s.encode('utf-8')) + roster_item = xmppim.RosterItem.fromElement(roster_item_elt) + self._jids[roster_jid] = roster_item + self._register_item(roster_item) + else: + log.warning(_("our server doesn't support roster versioning")) + version = None + + log.debug("requesting roster") + roster = yield self.getRoster(version=version) + if roster is None: + log.debug("empty roster result received, we'll get roster item with roster " + "pushes") + else: + # a full roster is received + self._groups.clear() + self._jids = roster + for item in roster.values(): + if not item.subscriptionTo and not item.subscriptionFrom and not item.ask: + # XXX: current behaviour: we don't want contact in our roster list + # if there is no presence subscription + # may change in the future + log.info( + "Removing contact {} from roster because there is no presence " + "subscription".format( + item.jid + ) + ) + self.removeItem(item.entity) # FIXME: to be checked + else: + self._register_item(item) + yield self._cache_roster(roster.version) + + if not self.got_roster.called: + # got_roster may already be called if we use resync() + self.got_roster.callback(None) + + def removeItem(self, to_jid): + """Remove a contact from roster list + @param to_jid: a JID instance + @return: Deferred + """ + return xmppim.RosterClientProtocol.removeItem(self, to_jid) + + def get_attributes(self, item): + """Return dictionary of attributes as used in bridge from a RosterItem + + @param item: RosterItem + @return: dictionary of attributes + """ + item_attr = { + "to": str(item.subscriptionTo), + "from": str(item.subscriptionFrom), + "ask": str(item.ask), + } + if item.name: + item_attr["name"] = item.name + return item_attr + + def setReceived(self, request): + item = request.item + entity = item.entity + log.info(_("adding {entity} to roster").format(entity=entity.full())) + if request.version is not None: + # we update the cache in storage + roster_cache = self.roster_cache + roster_cache[entity.full()] = item.toElement().toXml() + roster_cache[ROSTER_VER_KEY] = request.version + + try: # update the cache for the groups the contact has been removed from + left_groups = set(self._jids[entity].groups).difference(item.groups) + for group in left_groups: + jids_set = self._groups[group] + jids_set.remove(entity) + if not jids_set: + del self._groups[group] + except KeyError: + pass # no previous item registration (or it's been cleared) + self._jids[entity] = item + self._register_item(item) + self.host.bridge.contact_new( + entity.full(), self.get_attributes(item), list(item.groups), + self.parent.profile + ) + + def removeReceived(self, request): + entity = request.item.entity + log.info(_("removing {entity} from roster").format(entity=entity.full())) + if request.version is not None: + # we update the cache in storage + roster_cache = self.roster_cache + try: + del roster_cache[request.item.entity.full()] + except KeyError: + # because we don't use load(), cache won't have the key, but it + # will be deleted from storage anyway + pass + roster_cache[ROSTER_VER_KEY] = request.version + + # we first remove item from local cache (self._groups and self._jids) + try: + item = self._jids.pop(entity) + except KeyError: + log.error( + "Received a roster remove event for an item not in cache ({})".format( + entity + ) + ) + return + for group in item.groups: + try: + jids_set = self._groups[group] + jids_set.remove(entity) + if not jids_set: + del self._groups[group] + except KeyError: + log.warning( + f"there is no cache for the group [{group}] of the removed roster " + f"item [{entity}]" + ) + + # then we send the bridge signal + self.host.bridge.contact_deleted(entity.full(), self.parent.profile) + + def get_groups(self): + """Return a list of groups""" + return list(self._groups.keys()) + + def get_item(self, entity_jid): + """Return RosterItem for a given jid + + @param entity_jid(jid.JID): jid of the contact + @return(RosterItem, None): RosterItem instance + None if contact is not in cache + """ + return self._jids.get(entity_jid, None) + + def get_jids(self): + """Return all jids of the roster""" + return list(self._jids.keys()) + + def is_jid_in_roster(self, entity_jid): + """Return True if jid is in roster""" + if not isinstance(entity_jid, jid.JID): + raise exceptions.InternalError( + f"a JID is expected, not {type(entity_jid)}: {entity_jid!r}") + return entity_jid in self._jids + + def is_subscribed_from(self, entity_jid: jid.JID) -> bool: + """Return True if entity is authorised to see our presence""" + try: + item = self._jids[entity_jid.userhostJID()] + except KeyError: + return False + return item.subscriptionFrom + + def is_subscribed_to(self, entity_jid: jid.JID) -> bool: + """Return True if we are subscribed to entity""" + try: + item = self._jids[entity_jid.userhostJID()] + except KeyError: + return False + return item.subscriptionTo + + def get_items(self): + """Return all items of the roster""" + return list(self._jids.values()) + + def get_jids_from_group(self, group): + try: + return self._groups[group] + except KeyError: + raise exceptions.UnknownGroupError(group) + + def get_jids_set(self, type_, groups=None): + """Helper method to get a set of jids + + @param type_(unicode): one of: + C.ALL: get all jids from roster + C.GROUP: get jids from groups (listed in "groups") + @groups(list[unicode]): list of groups used if type_==C.GROUP + @return (set(jid.JID)): set of selected jids + """ + if type_ == C.ALL and groups is not None: + raise ValueError("groups must not be set for {} type".format(C.ALL)) + + if type_ == C.ALL: + return set(self.get_jids()) + elif type_ == C.GROUP: + jids = set() + for group in groups: + jids.update(self.get_jids_from_group(group)) + return jids + else: + raise ValueError("Unexpected type_ {}".format(type_)) + + def get_nick(self, entity_jid): + """Return a nick name for an entity + + return nick choosed by user if available + else return user part of entity_jid + """ + item = self.get_item(entity_jid) + if item is None: + return entity_jid.user + else: + return item.name or entity_jid.user + + +class SatPresenceProtocol(xmppim.PresenceClientProtocol): + + def __init__(self, host): + xmppim.PresenceClientProtocol.__init__(self) + self.host = host + + @property + def client(self): + return self.parent + + def send(self, obj): + presence_d = defer.succeed(None) + if not self.host.trigger.point("Presence send", self.parent, obj, presence_d): + return + presence_d.addCallback(lambda __: super(SatPresenceProtocol, self).send(obj)) + return presence_d + + def availableReceived(self, entity, show=None, statuses=None, priority=0): + if not statuses: + statuses = {} + + if None in statuses: # we only want string keys + statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop(None) + + if not self.host.trigger.point( + "presence_received", self.parent, entity, show, priority, statuses + ): + return + + self.host.memory.set_presence_status( + entity, show or "", int(priority), statuses, self.parent.profile + ) + + # now it's time to notify frontends + self.host.bridge.presence_update( + entity.full(), show or "", int(priority), statuses, self.parent.profile + ) + + def unavailableReceived(self, entity, statuses=None): + log.debug( + _("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") + % {"entity": entity, C.PRESENCE_STATUSES: statuses} + ) + + if not statuses: + statuses = {} + + if None in statuses: # we only want string keys + statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop(None) + + if not self.host.trigger.point( + "presence_received", self.parent, entity, C.PRESENCE_UNAVAILABLE, 0, statuses, + ): + return + + # now it's time to notify frontends + # if the entity is not known yet in this session or is already unavailable, + # there is no need to send an unavailable signal + try: + presence = self.host.memory.get_entity_datum( + self.client, entity, "presence" + ) + except (KeyError, exceptions.UnknownEntityError): + # the entity has not been seen yet in this session + pass + else: + if presence.show != C.PRESENCE_UNAVAILABLE: + self.host.bridge.presence_update( + entity.full(), + C.PRESENCE_UNAVAILABLE, + 0, + statuses, + self.parent.profile, + ) + + self.host.memory.set_presence_status( + entity, C.PRESENCE_UNAVAILABLE, 0, statuses, self.parent.profile + ) + + def available(self, entity=None, show=None, statuses=None, priority=None): + """Set a presence and statuses. + + @param entity (jid.JID): entity + @param show (unicode): value in ('unavailable', '', 'away', 'xa', 'chat', 'dnd') + @param statuses (dict{unicode: unicode}): multilingual statuses with + the entry key beeing a language code on 2 characters or "default". + """ + if priority is None: + try: + priority = int( + self.host.memory.param_get_a( + "Priority", "Connection", profile_key=self.parent.profile + ) + ) + except ValueError: + priority = 0 + + if statuses is None: + statuses = {} + + # default for us is None for wokkel + # so we must temporarily switch to wokkel's convention... + if C.PRESENCE_STATUSES_DEFAULT in statuses: + statuses[None] = statuses.pop(C.PRESENCE_STATUSES_DEFAULT) + + presence_elt = xmppim.AvailablePresence(entity, show, statuses, priority) + + # ... before switching back + if None in statuses: + statuses["default"] = statuses.pop(None) + + if not self.host.trigger.point("presence_available", presence_elt, self.parent): + return + return self.send(presence_elt) + + @defer.inlineCallbacks + def subscribed(self, entity): + yield self.parent.roster.got_roster + xmppim.PresenceClientProtocol.subscribed(self, entity) + self.host.memory.del_waiting_sub(entity.userhost(), self.parent.profile) + item = self.parent.roster.get_item(entity) + if ( + not item or not item.subscriptionTo + ): # we automatically subscribe to 'to' presence + log.debug(_('sending automatic "from" subscription request')) + self.subscribe(entity) + + def unsubscribed(self, entity): + xmppim.PresenceClientProtocol.unsubscribed(self, entity) + self.host.memory.del_waiting_sub(entity.userhost(), self.parent.profile) + + def subscribedReceived(self, entity): + log.debug(_("subscription approved for [%s]") % entity.userhost()) + self.host.bridge.subscribe("subscribed", entity.userhost(), self.parent.profile) + + def unsubscribedReceived(self, entity): + log.debug(_("unsubscription confirmed for [%s]") % entity.userhost()) + self.host.bridge.subscribe("unsubscribed", entity.userhost(), self.parent.profile) + + @defer.inlineCallbacks + def subscribeReceived(self, entity): + log.debug(_("subscription request from [%s]") % entity.userhost()) + yield self.parent.roster.got_roster + item = self.parent.roster.get_item(entity) + if item and item.subscriptionTo: + # We automatically accept subscription if we are already subscribed to + # contact presence + log.debug(_("sending automatic subscription acceptance")) + self.subscribed(entity) + else: + self.host.memory.add_waiting_sub( + "subscribe", entity.userhost(), self.parent.profile + ) + self.host.bridge.subscribe( + "subscribe", entity.userhost(), self.parent.profile + ) + + @defer.inlineCallbacks + def unsubscribeReceived(self, entity): + log.debug(_("unsubscription asked for [%s]") % entity.userhost()) + yield self.parent.roster.got_roster + item = self.parent.roster.get_item(entity) + if item and item.subscriptionFrom: # we automatically remove contact + log.debug(_("automatic contact deletion")) + self.host.contact_del(entity, self.parent.profile) + self.host.bridge.subscribe("unsubscribe", entity.userhost(), self.parent.profile) + + +@implementer(iwokkel.IDisco) +class SatDiscoProtocol(disco.DiscoClientProtocol): + + def __init__(self, host): + disco.DiscoClientProtocol.__init__(self) + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + # those features are implemented in Wokkel (or sat_tmp.wokkel) + # and thus are always available + return [disco.DiscoFeature(NS_X_DATA), + disco.DiscoFeature(NS_XML_ELEMENT), + disco.DiscoFeature(NS_DISCO_INFO)] + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return [] + + +class SatFallbackHandler(generic.FallbackHandler): + def __init__(self, host): + generic.FallbackHandler.__init__(self) + + def iqFallback(self, iq): + if iq.handled is True: + return + log.debug("iqFallback: xml = [%s]" % (iq.toXml())) + generic.FallbackHandler.iqFallback(self, iq) + + +class SatVersionHandler(generic.VersionHandler): + + def getDiscoInfo(self, requestor, target, node): + # XXX: We need to work around wokkel's behaviour (namespace not added if there + # is a node) as it cause issues with XEP-0115 & PEP (XEP-0163): there is a + # node when server ask for disco info, and not when we generate the key, so + # the hash is used with different disco features, and when the server (seen + # on ejabberd) generate its own hash for security check it reject our + # features (resulting in e.g. no notification on PEP) + return generic.VersionHandler.getDiscoInfo(self, requestor, target, None) + + +@implementer(iwokkel.IDisco) +class SatIdentityHandler(XMPPHandler): + """Manage disco Identity of SàT.""" + # TODO: dynamic identity update (see docstring). Note that a XMPP entity can have + # several identities + + def getDiscoInfo(self, requestor, target, nodeIdentifier=""): + return self.parent.identities + + def getDiscoItems(self, requestor, target, nodeIdentifier=""): + return [] diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/cache.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/cache.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from io import BufferedIOBase +import mimetypes +from pathlib import Path +import pickle as pickle +import time +from typing import Any, Dict, Optional + +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.tools.common import regex + + +log = getLogger(__name__) + +DEFAULT_EXT = ".raw" + + +class Cache(object): + """generic file caching""" + + def __init__(self, host, profile): + """ + @param profile(unicode, None): name of the profile to set the cache for + if None, the cache will be common for all profiles + """ + self.profile = profile + path_elts = [host.memory.config_get("", "local_dir"), C.CACHE_DIR] + if profile: + path_elts.extend(["profiles", regex.path_escape(profile)]) + else: + path_elts.append("common") + self.cache_dir = Path(*path_elts) + + self.cache_dir.mkdir(0o700, parents=True, exist_ok=True) + self.purge() + + def purge(self): + # remove expired files from cache + # TODO: this should not be called only on startup, but at regular interval + # (e.g. once a day) + purged = set() + # we sort files to have metadata files first + for cache_file in sorted(self.cache_dir.iterdir()): + if cache_file in purged: + continue + try: + with cache_file.open('rb') as f: + cache_data = pickle.load(f) + except IOError: + log.warning( + _("Can't read metadata file at {path}") + .format(path=cache_file)) + continue + except (pickle.UnpicklingError, EOFError): + log.debug(f"File at {cache_file} is not a metadata file") + continue + try: + eol = cache_data['eol'] + filename = cache_data['filename'] + except KeyError: + log.warning( + _("Invalid cache metadata at {path}") + .format(path=cache_file)) + continue + + filepath = self.getPath(filename) + + if not filepath.exists(): + log.warning(_( + "cache {cache_file!r} references an inexisting file: {filepath!r}" + ).format(cache_file=str(cache_file), filepath=str(filepath))) + log.debug("purging cache with missing file") + cache_file.unlink() + elif eol < time.time(): + log.debug( + "purging expired cache {filepath!r} (expired for {time}s)" + .format(filepath=str(filepath), time=int(time.time() - eol)) + ) + cache_file.unlink() + try: + filepath.unlink() + except FileNotFoundError: + log.warning( + _("following file is missing while purging cache: {path}") + .format(path=filepath) + ) + purged.add(cache_file) + purged.add(filepath) + + def getPath(self, filename: str) -> Path: + """return cached file URL + + @param filename: cached file name (cache data or actual file) + @return: path to the cached file + """ + if not filename or "/" in filename: + log.error( + "invalid char found in file name, hack attempt? name:{}".format(filename) + ) + raise exceptions.DataError("Invalid char found") + return self.cache_dir / filename + + def get_metadata(self, uid: str, update_eol: bool = True) -> Optional[Dict[str, Any]]: + """Retrieve metadata for cached data + + @param uid(unicode): unique identifier of file + @param update_eol(bool): True if eol must extended + if True, max_age will be added to eol (only if it is not already expired) + @return (dict, None): metadata with following keys: + see [cache_data] for data details, an additional "path" key is the full path to + cached file. + None if file is not in cache (or cache is invalid) + """ + + uid = uid.strip() + if not uid: + raise exceptions.InternalError("uid must not be empty") + cache_url = self.getPath(uid) + if not cache_url.exists(): + return None + + try: + with cache_url.open("rb") as f: + cache_data = pickle.load(f) + except (IOError, EOFError) as e: + log.warning(f"can't read cache at {cache_url}: {e}") + return None + except pickle.UnpicklingError: + log.warning(f"invalid cache found at {cache_url}") + return None + + try: + eol = cache_data["eol"] + except KeyError: + log.warning("no End Of Life found for cached file {}".format(uid)) + eol = 0 + if eol < time.time(): + log.debug( + "removing expired cache (expired for {}s)".format(time.time() - eol) + ) + return None + + if update_eol: + try: + max_age = cache_data["max_age"] + except KeyError: + log.warning(f"no max_age found for cache at {cache_url}, using default") + max_age = cache_data["max_age"] = C.DEFAULT_MAX_AGE + now = int(time.time()) + cache_data["last_access"] = now + cache_data["eol"] = now + max_age + with cache_url.open("wb") as f: + pickle.dump(cache_data, f, protocol=2) + + cache_data["path"] = self.getPath(cache_data["filename"]) + return cache_data + + def get_file_path(self, uid: str) -> Path: + """Retrieve absolute path to file + + @param uid(unicode): unique identifier of file + @return (unicode, None): absolute path to cached file + None if file is not in cache (or cache is invalid) + """ + metadata = self.get_metadata(uid) + if metadata is not None: + return metadata["path"] + + def remove_from_cache(self, uid, metadata=None): + """Remove data from cache + + @param uid(unicode): unique identifier cache file + """ + cache_data = self.get_metadata(uid, update_eol=False) + if cache_data is None: + log.debug(f"cache with uid {uid!r} has already expired or been removed") + return + + try: + filename = cache_data['filename'] + except KeyError: + log.warning(_("missing filename for cache {uid!r}") .format(uid=uid)) + else: + filepath = self.getPath(filename) + try: + filepath.unlink() + except FileNotFoundError: + log.warning( + _("missing file referenced in cache {uid!r}: {filename}") + .format(uid=uid, filename=filename) + ) + + cache_file = self.getPath(uid) + cache_file.unlink() + log.debug(f"cache with uid {uid!r} has been removed") + + def cache_data( + self, + source: str, + uid: str, + mime_type: Optional[str] = None, + max_age: Optional[int] = None, + original_filename: Optional[str] = None + ) -> BufferedIOBase: + """create cache metadata and file object to use for actual data + + @param source: source of the cache (should be plugin's import_name) + @param uid: an identifier of the file which must be unique + @param mime_type: MIME type of the file to cache + it will be used notably to guess file extension + It may be autogenerated if filename is specified + @param max_age: maximum age in seconds + the cache metadata will have an "eol" (end of life) + None to use default value + 0 to ignore cache (file will be re-downloaded on each access) + @param original_filename: if not None, will be used to retrieve file extension and + guess + mime type, and stored in "original_filename" + @return: file object opened in write mode + you have to close it yourself (hint: use ``with`` statement) + """ + if max_age is None: + max_age = C.DEFAULT_MAX_AGE + cache_data = { + "source": source, + # we also store max_age for updating eol + "max_age": max_age, + } + cache_url = self.getPath(uid) + if original_filename is not None: + cache_data["original_filename"] = original_filename + if mime_type is None: + # we have original_filename but not MIME type, we try to guess the later + mime_type = mimetypes.guess_type(original_filename, strict=False)[0] + if mime_type: + ext = mimetypes.guess_extension(mime_type, strict=False) + if ext is None: + log.warning( + "can't find extension for MIME type {}".format(mime_type) + ) + ext = DEFAULT_EXT + elif ext == ".jpe": + ext = ".jpg" + else: + ext = DEFAULT_EXT + mime_type = None + filename = uid + ext + now = int(time.time()) + cache_data.update({ + "filename": filename, + "creation": now, + "eol": now + max_age, + "mime_type": mime_type, + }) + file_path = self.getPath(filename) + + with open(cache_url, "wb") as f: + pickle.dump(cache_data, f, protocol=2) + + return file_path.open("wb") diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/crypto.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/crypto.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +# SAT: a jabber client +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) +# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from os import urandom +from base64 import b64encode, b64decode +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + + +crypto_backend = default_backend() + + +class BlockCipher: + + BLOCK_SIZE = 16 + MAX_KEY_SIZE = 32 + IV_SIZE = BLOCK_SIZE # initialization vector size, 16 bits + + @staticmethod + def encrypt(key, text, leave_empty=True): + """Encrypt a message. + + Based on http://stackoverflow.com/a/12525165 + + @param key (unicode): the encryption key + @param text (unicode): the text to encrypt + @param leave_empty (bool): if True, empty text will be returned "as is" + @return (D(str)): base-64 encoded encrypted message + """ + if leave_empty and text == "": + return "" + iv = BlockCipher.get_random_key() + key = key.encode() + key = ( + key[: BlockCipher.MAX_KEY_SIZE] + if len(key) >= BlockCipher.MAX_KEY_SIZE + else BlockCipher.pad(key) + ) + + cipher = Cipher(algorithms.AES(key), modes.CFB8(iv), backend=crypto_backend) + encryptor = cipher.encryptor() + encrypted = encryptor.update(BlockCipher.pad(text.encode())) + encryptor.finalize() + return b64encode(iv + encrypted).decode() + + @staticmethod + def decrypt(key, ciphertext, leave_empty=True): + """Decrypt a message. + + Based on http://stackoverflow.com/a/12525165 + + @param key (unicode): the decryption key + @param ciphertext (base-64 encoded str): the text to decrypt + @param leave_empty (bool): if True, empty ciphertext will be returned "as is" + @return: Deferred: str or None if the password could not be decrypted + """ + if leave_empty and ciphertext == "": + return "" + ciphertext = b64decode(ciphertext) + iv, ciphertext = ( + ciphertext[: BlockCipher.IV_SIZE], + ciphertext[BlockCipher.IV_SIZE :], + ) + key = key.encode() + key = ( + key[: BlockCipher.MAX_KEY_SIZE] + if len(key) >= BlockCipher.MAX_KEY_SIZE + else BlockCipher.pad(key) + ) + + cipher = Cipher(algorithms.AES(key), modes.CFB8(iv), backend=crypto_backend) + decryptor = cipher.decryptor() + decrypted = decryptor.update(ciphertext) + decryptor.finalize() + return BlockCipher.unpad(decrypted) + + @staticmethod + def get_random_key(size=None, base64=False): + """Return a random key suitable for block cipher encryption. + + Note: a good value for the key length is to make it as long as the block size. + + @param size: key length in bytes, positive or null (default: BlockCipher.IV_SIZE) + @param base64: if True, encode the result to base-64 + @return: str (eventually base-64 encoded) + """ + if size is None or size < 0: + size = BlockCipher.IV_SIZE + key = urandom(size) + return b64encode(key) if base64 else key + + @staticmethod + def pad(s): + """Method from http://stackoverflow.com/a/12525165""" + bs = BlockCipher.BLOCK_SIZE + return s + (bs - len(s) % bs) * (chr(bs - len(s) % bs)).encode() + + @staticmethod + def unpad(s): + """Method from http://stackoverflow.com/a/12525165""" + s = s.decode() + return s[0 : -ord(s[-1])] + + +class PasswordHasher: + + SALT_LEN = 16 # 128 bits + + @staticmethod + def hash(password, salt=None, leave_empty=True): + """Hash a password. + + @param password (str): the password to hash + @param salt (base-64 encoded str): if not None, use the given salt instead of a random value + @param leave_empty (bool): if True, empty password will be returned "as is" + @return: Deferred: base-64 encoded str + """ + if leave_empty and password == "": + return "" + salt = ( + b64decode(salt)[: PasswordHasher.SALT_LEN] + if salt + else urandom(PasswordHasher.SALT_LEN) + ) + + # we use PyCrypto's PBKDF2 arguments while porting to crytography, to stay + # compatible with existing installations. But this is temporary and we need + # to update them to more secure values. + kdf = PBKDF2HMAC( + # FIXME: SHA1() is not secure, it is used here for historical reasons + # and must be changed as soon as possible + algorithm=hashes.SHA1(), + length=16, + salt=salt, + iterations=1000, + backend=crypto_backend + ) + key = kdf.derive(password.encode()) + return b64encode(salt + key).decode() + + @staticmethod + def verify(attempt, pwd_hash): + """Verify a password attempt. + + @param attempt (str): the attempt to check + @param pwd_hash (str): the hash of the password + @return: Deferred: boolean + """ + assert isinstance(attempt, str) + assert isinstance(pwd_hash, str) + leave_empty = pwd_hash == "" + attempt_hash = PasswordHasher.hash(attempt, pwd_hash, leave_empty) + assert isinstance(attempt_hash, str) + return attempt_hash == pwd_hash diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/disco.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/disco.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,499 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from typing import Optional +from libervia.backend.core.i18n import _ +from libervia.backend.core import exceptions +from libervia.backend.core.log import getLogger +from libervia.backend.core.core_types import SatXMPPEntity + +from twisted.words.protocols.jabber import jid +from twisted.words.protocols.jabber.error import StanzaError +from twisted.internet import defer +from twisted.internet import reactor +from twisted.python import failure +from libervia.backend.core.constants import Const as C +from libervia.backend.tools import xml_tools +from libervia.backend.memory import persistent +from wokkel import disco +from base64 import b64encode +from hashlib import sha1 + + +log = getLogger(__name__) + + +TIMEOUT = 15 +CAP_HASH_ERROR = "ERROR" + + +class HashGenerationError(Exception): + pass + + +class ByteIdentity(object): + """This class manage identity as bytes (needed for i;octet sort), it is used for the hash generation""" + + def __init__(self, identity, lang=None): + assert isinstance(identity, disco.DiscoIdentity) + self.category = identity.category.encode("utf-8") + self.idType = identity.type.encode("utf-8") + self.name = identity.name.encode("utf-8") if identity.name else b"" + self.lang = lang.encode("utf-8") if lang is not None else b"" + + def __bytes__(self): + return b"%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name) + + +class HashManager(object): + """map object which manage hashes + + persistent storage is update when a new hash is added + """ + + def __init__(self, persistent): + self.hashes = { + CAP_HASH_ERROR: disco.DiscoInfo() # used when we can't get disco infos + } + self.persistent = persistent + + def __getitem__(self, key): + return self.hashes[key] + + def __setitem__(self, hash_, disco_info): + if hash_ in self.hashes: + log.debug("ignoring hash set: it is already known") + return + self.hashes[hash_] = disco_info + self.persistent[hash_] = disco_info.toElement().toXml() + + def __contains__(self, hash_): + return self.hashes.__contains__(hash_) + + def load(self): + def fill_hashes(hashes): + for hash_, xml in hashes.items(): + element = xml_tools.ElementParser()(xml) + disco_info = disco.DiscoInfo.fromElement(element) + for ext_form in disco_info.extensions.values(): + # wokkel doesn't call typeCheck on reception, so we do it here + ext_form.typeCheck() + if not disco_info.features and not disco_info.identities: + log.warning( + _( + "no feature/identity found in disco element (hash: {cap_hash}), ignoring: {xml}" + ).format(cap_hash=hash_, xml=xml) + ) + else: + self.hashes[hash_] = disco_info + + log.info("Disco hashes loaded") + + d = self.persistent.load() + d.addCallback(fill_hashes) + return d + + +class Discovery(object): + """ Manage capabilities of entities """ + + def __init__(self, host): + self.host = host + # TODO: remove legacy hashes + + def load(self): + """Load persistent hashes""" + self.hashes = HashManager(persistent.PersistentDict("disco")) + return self.hashes.load() + + @defer.inlineCallbacks + def hasFeature(self, client, feature, jid_=None, node=""): + """Tell if an entity has the required feature + + @param feature: feature namespace + @param jid_: jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + @return: a Deferred which fire a boolean (True if feature is available) + """ + disco_infos = yield self.get_infos(client, jid_, node) + defer.returnValue(feature in disco_infos.features) + + @defer.inlineCallbacks + def check_feature(self, client, feature, jid_=None, node=""): + """Like hasFeature, but raise an exception is feature is not Found + + @param feature: feature namespace + @param jid_: jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + + @raise: exceptions.FeatureNotFound + """ + disco_infos = yield self.get_infos(client, jid_, node) + if not feature in disco_infos.features: + raise failure.Failure(exceptions.FeatureNotFound()) + + @defer.inlineCallbacks + def check_features(self, client, features, jid_=None, identity=None, node=""): + """Like check_feature, but check several features at once, and check also identity + + @param features(iterable[unicode]): features to check + @param jid_(jid.JID): jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + @param identity(None, tuple(unicode, unicode): if not None, the entity must have an identity with this (category, type) tuple + + @raise: exceptions.FeatureNotFound + """ + disco_infos = yield self.get_infos(client, jid_, node) + if not set(features).issubset(disco_infos.features): + raise failure.Failure(exceptions.FeatureNotFound()) + + if identity is not None and identity not in disco_infos.identities: + raise failure.Failure(exceptions.FeatureNotFound()) + + async def has_identity( + self, + client: SatXMPPEntity, + category: str, + type_: str, + jid_: Optional[jid.JID] = None, + node: str = "" + ) -> bool: + """Tell if an entity has the requested identity + + @param category: identity category + @param type_: identity type + @param jid_: jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + @return: True if the entity has the given identity + """ + disco_infos = await self.get_infos(client, jid_, node) + return (category, type_) in disco_infos.identities + + def get_infos(self, client, jid_=None, node="", use_cache=True): + """get disco infos from jid_, filling capability hash if needed + + @param jid_: jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + @param use_cache(bool): if True, use cached data if available + @return: a Deferred which fire disco.DiscoInfo + """ + if jid_ is None: + jid_ = jid.JID(client.jid.host) + try: + if not use_cache: + # we ignore cache, so we pretend we haven't found it + raise KeyError + cap_hash = self.host.memory.entity_data_get( + client, jid_, [C.ENTITY_CAP_HASH] + )[C.ENTITY_CAP_HASH] + except (KeyError, exceptions.UnknownEntityError): + # capability hash is not available, we'll compute one + def infos_cb(disco_infos): + cap_hash = self.generate_hash(disco_infos) + for ext_form in disco_infos.extensions.values(): + # wokkel doesn't call typeCheck on reception, so we do it here + # to avoid ending up with incorrect types. We have to do it after + # the hash has been generated (str value is needed to compute the + # hash) + ext_form.typeCheck() + self.hashes[cap_hash] = disco_infos + self.host.memory.update_entity_data( + client, jid_, C.ENTITY_CAP_HASH, cap_hash + ) + return disco_infos + + def infos_eb(fail): + if fail.check(defer.CancelledError): + reason = "request time-out" + fail = failure.Failure(exceptions.TimeOutError(str(fail.value))) + else: + try: + reason = str(fail.value) + except AttributeError: + reason = str(fail) + + log.warning( + "can't request disco infos from {jid}: {reason}".format( + jid=jid_.full(), reason=reason + ) + ) + + # XXX we set empty disco in cache, to avoid getting an error or waiting + # for a timeout again the next time + self.host.memory.update_entity_data( + client, jid_, C.ENTITY_CAP_HASH, CAP_HASH_ERROR + ) + raise fail + + d = client.disco.requestInfo(jid_, nodeIdentifier=node) + d.addCallback(infos_cb) + d.addErrback(infos_eb) + return d + else: + disco_infos = self.hashes[cap_hash] + return defer.succeed(disco_infos) + + @defer.inlineCallbacks + def get_items(self, client, jid_=None, node="", use_cache=True): + """get disco items from jid_, cache them for our own server + + @param jid_(jid.JID): jid of the target, or None for profile's server + @param node(unicode): optional node to use for disco request + @param use_cache(bool): if True, use cached data if available + @return: a Deferred which fire disco.DiscoItems + """ + if jid_ is None: + jid_ = client.server_jid + + if jid_ == client.server_jid and not node: + # we cache items only for our own server and if node is not set + try: + items = self.host.memory.entity_data_get( + client, jid_, ["DISCO_ITEMS"] + )["DISCO_ITEMS"] + log.debug("[%s] disco items are in cache" % jid_.full()) + if not use_cache: + # we ignore cache, so we pretend we haven't found it + raise KeyError + except (KeyError, exceptions.UnknownEntityError): + log.debug("Caching [%s] disco items" % jid_.full()) + items = yield client.disco.requestItems(jid_, nodeIdentifier=node) + self.host.memory.update_entity_data( + client, jid_, "DISCO_ITEMS", items + ) + else: + try: + items = yield client.disco.requestItems(jid_, nodeIdentifier=node) + except StanzaError as e: + log.warning( + "Error while requesting items for {jid}: {reason}".format( + jid=jid_.full(), reason=e.condition + ) + ) + items = disco.DiscoItems() + + defer.returnValue(items) + + def _infos_eb(self, failure_, entity_jid): + failure_.trap(StanzaError) + log.warning( + _("Error while requesting [%(jid)s]: %(error)s") + % {"jid": entity_jid.full(), "error": failure_.getErrorMessage()} + ) + + def find_service_entity(self, client, category, type_, jid_=None): + """Helper method to find first available entity from find_service_entities + + args are the same as for [find_service_entities] + @return (jid.JID, None): found entity + """ + d = self.host.find_service_entities(client, category, type_) + d.addCallback(lambda entities: entities.pop() if entities else None) + return d + + def find_service_entities(self, client, category, type_, jid_=None): + """Return all available items of an entity which correspond to (category, type_) + + @param category: identity's category + @param type_: identitiy's type + @param jid_: the jid of the target server (None for profile's server) + @return: a set of found entities + @raise defer.CancelledError: the request timed out + """ + found_entities = set() + + def infos_cb(infos, entity_jid): + if (category, type_) in infos.identities: + found_entities.add(entity_jid) + + def got_items(items): + defers_list = [] + for item in items: + info_d = self.get_infos(client, item.entity) + info_d.addCallbacks( + infos_cb, self._infos_eb, [item.entity], None, [item.entity] + ) + defers_list.append(info_d) + return defer.DeferredList(defers_list) + + d = self.get_items(client, jid_) + d.addCallback(got_items) + d.addCallback(lambda __: found_entities) + reactor.callLater( + TIMEOUT, d.cancel + ) # FIXME: one bad service make a general timeout + return d + + def find_features_set(self, client, features, identity=None, jid_=None): + """Return entities (including jid_ and its items) offering features + + @param features: iterable of features which must be present + @param identity(None, tuple(unicode, unicode)): if not None, accept only this + (category/type) identity + @param jid_: the jid of the target server (None for profile's server) + @param profile: %(doc_profile)s + @return: a set of found entities + """ + if jid_ is None: + jid_ = jid.JID(client.jid.host) + features = set(features) + found_entities = set() + + def infos_cb(infos, entity): + if entity is None: + log.warning(_("received an item without jid")) + return + if identity is not None and identity not in infos.identities: + return + if features.issubset(infos.features): + found_entities.add(entity) + + def got_items(items): + defer_list = [] + for entity in [jid_] + [item.entity for item in items]: + infos_d = self.get_infos(client, entity) + infos_d.addCallbacks(infos_cb, self._infos_eb, [entity], None, [entity]) + defer_list.append(infos_d) + return defer.DeferredList(defer_list) + + d = self.get_items(client, jid_) + d.addCallback(got_items) + d.addCallback(lambda __: found_entities) + reactor.callLater( + TIMEOUT, d.cancel + ) # FIXME: one bad service make a general timeout + return d + + def generate_hash(self, services): + """ Generate a unique hash for given service + + hash algorithm is the one described in XEP-0115 + @param services: iterable of disco.DiscoIdentity/disco.DiscoFeature, as returned by discoHandler.info + + """ + s = [] + # identities + byte_identities = [ + ByteIdentity(service) + for service in services + if isinstance(service, disco.DiscoIdentity) + ] # FIXME: lang must be managed here + byte_identities.sort(key=lambda i: i.lang) + byte_identities.sort(key=lambda i: i.idType) + byte_identities.sort(key=lambda i: i.category) + for identity in byte_identities: + s.append(bytes(identity)) + s.append(b"<") + # features + byte_features = [ + service.encode("utf-8") + for service in services + if isinstance(service, disco.DiscoFeature) + ] + byte_features.sort() # XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort + for feature in byte_features: + s.append(feature) + s.append(b"<") + + # extensions + ext = list(services.extensions.values()) + ext.sort(key=lambda f: f.formNamespace.encode('utf-8')) + for extension in ext: + s.append(extension.formNamespace.encode('utf-8')) + s.append(b"<") + fields = extension.fieldList + fields.sort(key=lambda f: f.var.encode('utf-8')) + for field in fields: + s.append(field.var.encode('utf-8')) + s.append(b"<") + values = [v.encode('utf-8') for v in field.values] + values.sort() + for value in values: + s.append(value) + s.append(b"<") + + cap_hash = b64encode(sha1(b"".join(s)).digest()).decode('utf-8') + log.debug(_("Capability hash generated: [{cap_hash}]").format(cap_hash=cap_hash)) + return cap_hash + + @defer.inlineCallbacks + def _disco_infos( + self, entity_jid_s, node="", use_cache=True, profile_key=C.PROF_KEY_NONE + ): + """Discovery method for the bridge + @param entity_jid_s: entity we want to discover + @param use_cache(bool): if True, use cached data if available + @param node(unicode): optional node to use + + @return: list of tuples + """ + client = self.host.get_client(profile_key) + entity = jid.JID(entity_jid_s) + disco_infos = yield self.get_infos(client, entity, node, use_cache) + extensions = {} + # FIXME: should extensions be serialised using tools.common.data_format? + for form_type, form in list(disco_infos.extensions.items()): + fields = [] + for field in form.fieldList: + data = {"type": field.fieldType} + for attr in ("var", "label", "desc"): + value = getattr(field, attr) + if value is not None: + data[attr] = value + + values = [field.value] if field.value is not None else field.values + if field.fieldType == "boolean": + values = [C.bool_const(v) for v in values] + fields.append((data, values)) + + extensions[form_type or ""] = fields + + defer.returnValue(( + [str(f) for f in disco_infos.features], + [(cat, type_, name or "") + for (cat, type_), name in list(disco_infos.identities.items())], + extensions)) + + def items2tuples(self, disco_items): + """convert disco items to tuple of strings + + @param disco_items(iterable[disco.DiscoItem]): items + @return G(tuple[unicode,unicode,unicode]): serialised items + """ + for item in disco_items: + if not item.entity: + log.warning(_("invalid item (no jid)")) + continue + yield (item.entity.full(), item.nodeIdentifier or "", item.name or "") + + @defer.inlineCallbacks + def _disco_items( + self, entity_jid_s, node="", use_cache=True, profile_key=C.PROF_KEY_NONE + ): + """ Discovery method for the bridge + + @param entity_jid_s: entity we want to discover + @param node(unicode): optional node to use + @param use_cache(bool): if True, use cached data if available + @return: list of tuples""" + client = self.host.get_client(profile_key) + entity = jid.JID(entity_jid_s) + disco_items = yield self.get_items(client, entity, node, use_cache) + ret = list(self.items2tuples(disco_items)) + defer.returnValue(ret) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/encryption.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/encryption.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,534 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import copy +from functools import partial +from typing import Optional +from twisted.words.protocols.jabber import jid +from twisted.internet import defer +from twisted.python import failure +from libervia.backend.core.core_types import EncryptionPlugin, EncryptionSession, MessageData +from libervia.backend.core.i18n import D_, _ +from libervia.backend.core.constants import Const as C +from libervia.backend.core import exceptions +from libervia.backend.core.log import getLogger +from libervia.backend.tools.common import data_format +from libervia.backend.tools import utils +from libervia.backend.memory import persistent + + +log = getLogger(__name__) + + +class EncryptionHandler: + """Class to handle encryption sessions for a client""" + plugins = [] # plugin able to encrypt messages + + def __init__(self, client): + self.client = client + self._sessions = {} # bare_jid ==> encryption_data + self._stored_session = persistent.PersistentDict( + "core:encryption", profile=client.profile) + + @property + def host(self): + return self.client.host_app + + async def load_sessions(self): + """Load persistent sessions""" + await self._stored_session.load() + start_d_list = [] + for entity_jid_s, namespace in self._stored_session.items(): + entity = jid.JID(entity_jid_s) + start_d_list.append(defer.ensureDeferred(self.start(entity, namespace))) + + if start_d_list: + result = await defer.DeferredList(start_d_list) + for idx, (success, err) in enumerate(result): + if not success: + entity_jid_s, namespace = list(self._stored_session.items())[idx] + log.warning(_( + "Could not restart {namespace!r} encryption with {entity}: {err}" + ).format(namespace=namespace, entity=entity_jid_s, err=err)) + log.info(_("encryption sessions restored")) + + @classmethod + def register_plugin(cls, plg_instance, name, namespace, priority=0, directed=False): + """Register a plugin handling an encryption algorithm + + @param plg_instance(object): instance of the plugin + it must have the following methods: + - get_trust_ui(entity): return a XMLUI for trust management + entity(jid.JID): entity to manage + The returned XMLUI must be a form + if may have the following methods: + - start_encryption(entity): start encrypted session + entity(jid.JID): entity to start encrypted session with + - stop_encryption(entity): start encrypted session + entity(jid.JID): entity to stop encrypted session with + if they don't exists, those 2 methods will be ignored. + + @param name(unicode): human readable name of the encryption algorithm + @param namespace(unicode): namespace of the encryption algorithm + @param priority(int): priority of this plugin to encrypt an message when not + selected manually + @param directed(bool): True if this plugin is directed (if it works with one + device only at a time) + """ + existing_ns = set() + existing_names = set() + for p in cls.plugins: + existing_ns.add(p.namespace.lower()) + existing_names.add(p.name.lower()) + if namespace.lower() in existing_ns: + raise exceptions.ConflictError("A plugin with this namespace already exists!") + if name.lower() in existing_names: + raise exceptions.ConflictError("A plugin with this name already exists!") + plugin = EncryptionPlugin( + instance=plg_instance, + name=name, + namespace=namespace, + priority=priority, + directed=directed) + cls.plugins.append(plugin) + cls.plugins.sort(key=lambda p: p.priority) + log.info(_("Encryption plugin registered: {name}").format(name=name)) + + @classmethod + def getPlugins(cls): + return cls.plugins + + @classmethod + def get_plugin(cls, namespace): + try: + return next(p for p in cls.plugins if p.namespace == namespace) + except StopIteration: + raise exceptions.NotFound(_( + "Can't find requested encryption plugin: {namespace}").format( + namespace=namespace)) + + @classmethod + def get_namespaces(cls): + """Get available plugin namespaces""" + return {p.namespace for p in cls.getPlugins()} + + @classmethod + def get_ns_from_name(cls, name): + """Retrieve plugin namespace from its name + + @param name(unicode): name of the plugin (case insensitive) + @return (unicode): namespace of the plugin + @raise exceptions.NotFound: there is not encryption plugin of this name + """ + for p in cls.plugins: + if p.name.lower() == name.lower(): + return p.namespace + raise exceptions.NotFound(_( + "Can't find a plugin with the name \"{name}\".".format( + name=name))) + + def get_bridge_data(self, session): + """Retrieve session data serialized for bridge. + + @param session(dict): encryption session + @return (unicode): serialized data for bridge + """ + if session is None: + return '' + plugin = session['plugin'] + bridge_data = {'name': plugin.name, + 'namespace': plugin.namespace} + if 'directed_devices' in session: + bridge_data['directed_devices'] = session['directed_devices'] + + return data_format.serialise(bridge_data) + + async def _start_encryption(self, plugin, entity): + """Start encryption with a plugin + + This method must be called just before adding a plugin session. + StartEncryptionn method of plugin will be called if it exists. + """ + if not plugin.directed: + await self._stored_session.aset(entity.userhost(), plugin.namespace) + try: + start_encryption = plugin.instance.start_encryption + except AttributeError: + log.debug(f"No start_encryption method found for {plugin.namespace}") + else: + # we copy entity to avoid having the resource changed by stop_encryption + await utils.as_deferred(start_encryption, self.client, copy.copy(entity)) + + async def _stop_encryption(self, plugin, entity): + """Stop encryption with a plugin + + This method must be called just before removing a plugin session. + StopEncryptionn method of plugin will be called if it exists. + """ + try: + await self._stored_session.adel(entity.userhost()) + except KeyError: + pass + try: + stop_encryption = plugin.instance.stop_encryption + except AttributeError: + log.debug(f"No stop_encryption method found for {plugin.namespace}") + else: + # we copy entity to avoid having the resource changed by stop_encryption + return utils.as_deferred(stop_encryption, self.client, copy.copy(entity)) + + async def start(self, entity, namespace=None, replace=False): + """Start an encryption session with an entity + + @param entity(jid.JID): entity to start an encryption session with + must be bare jid is the algorithm encrypt for all devices + @param namespace(unicode, None): namespace of the encryption algorithm + to use. + None to select automatically an algorithm + @param replace(bool): if True and an encrypted session already exists, + it will be replaced by the new one + """ + if not self.plugins: + raise exceptions.NotFound(_("No encryption plugin is registered, " + "an encryption session can't be started")) + + if namespace is None: + plugin = self.plugins[0] + else: + plugin = self.get_plugin(namespace) + + bare_jid = entity.userhostJID() + if bare_jid in self._sessions: + # we have already an encryption session with this contact + former_plugin = self._sessions[bare_jid]["plugin"] + if former_plugin.namespace == namespace: + log.info(_("Session with {bare_jid} is already encrypted with {name}. " + "Nothing to do.").format( + bare_jid=bare_jid, name=former_plugin.name)) + return + + if replace: + # there is a conflict, but replacement is requested + # so we stop previous encryption to use new one + del self._sessions[bare_jid] + await self._stop_encryption(former_plugin, entity) + else: + msg = (_("Session with {bare_jid} is already encrypted with {name}. " + "Please stop encryption session before changing algorithm.") + .format(bare_jid=bare_jid, name=plugin.name)) + log.warning(msg) + raise exceptions.ConflictError(msg) + + data = {"plugin": plugin} + if plugin.directed: + if not entity.resource: + entity.resource = self.host.memory.main_resource_get(self.client, entity) + if not entity.resource: + raise exceptions.NotFound( + _("No resource found for {destinee}, can't encrypt with {name}") + .format(destinee=entity.full(), name=plugin.name)) + log.info(_("No resource specified to encrypt with {name}, using " + "{destinee}.").format(destinee=entity.full(), + name=plugin.name)) + # indicate that we encrypt only for some devices + directed_devices = data['directed_devices'] = [entity.resource] + elif entity.resource: + raise ValueError(_("{name} encryption must be used with bare jids.")) + + await self._start_encryption(plugin, entity) + self._sessions[entity.userhostJID()] = data + log.info(_("Encryption session has been set for {entity_jid} with " + "{encryption_name}").format( + entity_jid=entity.full(), encryption_name=plugin.name)) + self.host.bridge.message_encryption_started( + entity.full(), + self.get_bridge_data(data), + self.client.profile) + msg = D_("Encryption session started: your messages with {destinee} are " + "now end to end encrypted using {name} algorithm.").format( + destinee=entity.full(), name=plugin.name) + directed_devices = data.get('directed_devices') + if directed_devices: + msg += "\n" + D_("Message are encrypted only for {nb_devices} device(s): " + "{devices_list}.").format( + nb_devices=len(directed_devices), + devices_list = ', '.join(directed_devices)) + + self.client.feedback(bare_jid, msg) + + async def stop(self, entity, namespace=None): + """Stop an encryption session with an entity + + @param entity(jid.JID): entity with who the encryption session must be stopped + must be bare jid if the algorithm encrypt for all devices + @param namespace(unicode): namespace of the session to stop + when specified, used to check that we stop the right encryption session + """ + session = self.getSession(entity.userhostJID()) + if not session: + raise failure.Failure( + exceptions.NotFound(_("There is no encryption session with this " + "entity."))) + plugin = session['plugin'] + if namespace is not None and plugin.namespace != namespace: + raise exceptions.InternalError(_( + "The encryption session is not run with the expected plugin: encrypted " + "with {current_name} and was expecting {expected_name}").format( + current_name=session['plugin'].namespace, + expected_name=namespace)) + if entity.resource: + try: + directed_devices = session['directed_devices'] + except KeyError: + raise exceptions.NotFound(_( + "There is a session for the whole entity (i.e. all devices of the " + "entity), not a directed one. Please use bare jid if you want to " + "stop the whole encryption with this entity.")) + + try: + directed_devices.remove(entity.resource) + except ValueError: + raise exceptions.NotFound(_("There is no directed session with this " + "entity.")) + else: + if not directed_devices: + # if we have no more directed device sessions, + # we stop the whole session + # see comment below for deleting session before stopping encryption + del self._sessions[entity.userhostJID()] + await self._stop_encryption(plugin, entity) + else: + # plugin's stop_encryption may call stop again (that's the case with OTR) + # so we need to remove plugin from session before calling self._stop_encryption + del self._sessions[entity.userhostJID()] + await self._stop_encryption(plugin, entity) + + log.info(_("encryption session stopped with entity {entity}").format( + entity=entity.full())) + self.host.bridge.message_encryption_stopped( + entity.full(), + {'name': plugin.name, + 'namespace': plugin.namespace, + }, + self.client.profile) + msg = D_("Encryption session finished: your messages with {destinee} are " + "NOT end to end encrypted anymore.\nYour server administrators or " + "{destinee} server administrators will be able to read them.").format( + destinee=entity.full()) + + self.client.feedback(entity, msg) + + def getSession(self, entity: jid.JID) -> Optional[EncryptionSession]: + """Get encryption session for this contact + + @param entity(jid.JID): get the session for this entity + must be a bare jid + @return (dict, None): encryption session data + None if there is not encryption for this session with this jid + """ + if entity.resource: + raise ValueError("Full jid given when expecting bare jid") + return self._sessions.get(entity) + + def get_namespace(self, entity: jid.JID) -> Optional[str]: + """Helper method to get the current encryption namespace used + + @param entity: get the namespace for this entity must be a bare jid + @return: the algorithm namespace currently used in this session, or None if no + e2ee is currently used. + """ + session = self.getSession(entity) + if session is None: + return None + return session["plugin"].namespace + + def get_trust_ui(self, entity_jid, namespace=None): + """Retrieve encryption UI + + @param entity_jid(jid.JID): get the UI for this entity + must be a bare jid + @param namespace(unicode): namespace of the algorithm to manage + if None use current algorithm + @return D(xmlui): XMLUI for trust management + the xmlui is a form + None if there is not encryption for this session with this jid + @raise exceptions.NotFound: no algorithm/plugin found + @raise NotImplementedError: plugin doesn't handle UI management + """ + if namespace is None: + session = self.getSession(entity_jid) + if not session: + raise exceptions.NotFound( + "No encryption session currently active for {entity_jid}" + .format(entity_jid=entity_jid.full())) + plugin = session['plugin'] + else: + plugin = self.get_plugin(namespace) + try: + get_trust_ui = plugin.instance.get_trust_ui + except AttributeError: + raise NotImplementedError( + "Encryption plugin doesn't handle trust management UI") + else: + return utils.as_deferred(get_trust_ui, self.client, entity_jid) + + ## Menus ## + + @classmethod + def _import_menus(cls, host): + host.import_menu( + (D_("Encryption"), D_("unencrypted (plain text)")), + partial(cls._on_menu_unencrypted, host=host), + security_limit=0, + help_string=D_("End encrypted session"), + type_=C.MENU_SINGLE, + ) + for plg in cls.getPlugins(): + host.import_menu( + (D_("Encryption"), plg.name), + partial(cls._on_menu_name, host=host, plg=plg), + security_limit=0, + help_string=D_("Start {name} session").format(name=plg.name), + type_=C.MENU_SINGLE, + ) + host.import_menu( + (D_("Encryption"), D_("⛨ {name} trust").format(name=plg.name)), + partial(cls._on_menu_trust, host=host, plg=plg), + security_limit=0, + help_string=D_("Manage {name} trust").format(name=plg.name), + type_=C.MENU_SINGLE, + ) + + @classmethod + def _on_menu_unencrypted(cls, data, host, profile): + client = host.get_client(profile) + peer_jid = jid.JID(data['jid']).userhostJID() + d = defer.ensureDeferred(client.encryption.stop(peer_jid)) + d.addCallback(lambda __: {}) + return d + + @classmethod + def _on_menu_name(cls, data, host, plg, profile): + client = host.get_client(profile) + peer_jid = jid.JID(data['jid']) + if not plg.directed: + peer_jid = peer_jid.userhostJID() + d = defer.ensureDeferred( + client.encryption.start(peer_jid, plg.namespace, replace=True)) + d.addCallback(lambda __: {}) + return d + + @classmethod + @defer.inlineCallbacks + def _on_menu_trust(cls, data, host, plg, profile): + client = host.get_client(profile) + peer_jid = jid.JID(data['jid']).userhostJID() + ui = yield client.encryption.get_trust_ui(peer_jid, plg.namespace) + defer.returnValue({'xmlui': ui.toXml()}) + + ## Triggers ## + + def set_encryption_flag(self, mess_data): + """Set "encryption" key in mess_data if session with destinee is encrypted""" + to_jid = mess_data['to'] + encryption = self._sessions.get(to_jid.userhostJID()) + if encryption is not None: + plugin = encryption['plugin'] + if mess_data["type"] == "groupchat" and plugin.directed: + raise exceptions.InternalError( + f"encryption flag must not be set for groupchat if encryption algorithm " + f"({encryption['plugin'].name}) is directed!") + mess_data[C.MESS_KEY_ENCRYPTION] = encryption + self.mark_as_encrypted(mess_data, plugin.namespace) + + ## Misc ## + + def mark_as_encrypted(self, mess_data, namespace): + """Helper method to mark a message as having been e2e encrypted. + + This should be used in the post_treat workflow of message_received trigger of + the plugin + @param mess_data(dict): message data as used in post treat workflow + @param namespace(str): namespace of the algorithm used for encrypting the message + """ + mess_data['extra'][C.MESS_KEY_ENCRYPTED] = True + from_bare_jid = mess_data['from'].userhostJID() + if from_bare_jid != self.client.jid.userhostJID(): + session = self.getSession(from_bare_jid) + if session is None: + # if we are currently unencrypted, we start a session automatically + # to avoid sending unencrypted messages in an encrypted context + log.info(_( + "Starting e2e session with {peer_jid} as we receive encrypted " + "messages") + .format(peer_jid=from_bare_jid) + ) + defer.ensureDeferred(self.start(from_bare_jid, namespace)) + + return mess_data + + def is_encryption_requested( + self, + mess_data: MessageData, + namespace: Optional[str] = None + ) -> bool: + """Helper method to check if encryption is requested in an outgoind message + + @param mess_data: message data for outgoing message + @param namespace: if set, check if encryption is requested for the algorithm + specified + @return: True if the encryption flag is present + """ + encryption = mess_data.get(C.MESS_KEY_ENCRYPTION) + if encryption is None: + return False + # we get plugin even if namespace is None to be sure that the key exists + plugin = encryption['plugin'] + if namespace is None: + return True + return plugin.namespace == namespace + + def isEncrypted(self, mess_data): + """Helper method to check if a message has the e2e encrypted flag + + @param mess_data(dict): message data + @return (bool): True if the encrypted flag is present + """ + return mess_data['extra'].get(C.MESS_KEY_ENCRYPTED, False) + + + def mark_as_trusted(self, mess_data): + """Helper methor to mark a message as sent from a trusted entity. + + This should be used in the post_treat workflow of message_received trigger of + the plugin + @param mess_data(dict): message data as used in post treat workflow + """ + mess_data[C.MESS_KEY_TRUSTED] = True + return mess_data + + def mark_as_untrusted(self, mess_data): + """Helper methor to mark a message as sent from an untrusted entity. + + This should be used in the post_treat workflow of message_received trigger of + the plugin + @param mess_data(dict): message data as used in post treat workflow + """ + mess_data['trusted'] = False + return mess_data diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/memory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/memory.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1881 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os.path +import copy +import shortuuid +import mimetypes +import time +from functools import partial +from typing import Optional, Tuple, Dict +from pathlib import Path +from uuid import uuid4 +from collections import namedtuple +from twisted.python import failure +from twisted.internet import defer, reactor, error +from twisted.words.protocols.jabber import jid +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.memory.sqla import Storage +from libervia.backend.memory.persistent import PersistentDict +from libervia.backend.memory.params import Params +from libervia.backend.memory.disco import Discovery +from libervia.backend.memory.crypto import BlockCipher +from libervia.backend.memory.crypto import PasswordHasher +from libervia.backend.tools import config as tools_config +from libervia.backend.tools.common import data_format +from libervia.backend.tools.common import regex + + +log = getLogger(__name__) + + +PresenceTuple = namedtuple("PresenceTuple", ("show", "priority", "statuses")) +MSG_NO_SESSION = "Session id doesn't exist or is finished" + + +class Sessions(object): + """Sessions are data associated to key used for a temporary moment, with optional profile checking.""" + + DEFAULT_TIMEOUT = 600 + + def __init__(self, timeout=None, resettable_timeout=True): + """ + @param timeout (int): nb of seconds before session destruction + @param resettable_timeout (bool): if True, the timeout is reset on each access + """ + self._sessions = dict() + self.timeout = timeout or Sessions.DEFAULT_TIMEOUT + self.resettable_timeout = resettable_timeout + + def new_session(self, session_data=None, session_id=None, profile=None): + """Create a new session + + @param session_data: mutable data to use, default to a dict + @param session_id (str): force the session_id to the given string + @param profile: if set, the session is owned by the profile, + and profile_get must be used instead of __getitem__ + @return: session_id, session_data + """ + if session_id is None: + session_id = str(uuid4()) + elif session_id in self._sessions: + raise exceptions.ConflictError( + "Session id {} is already used".format(session_id) + ) + timer = reactor.callLater(self.timeout, self._purge_session, session_id) + if session_data is None: + session_data = {} + self._sessions[session_id] = ( + (timer, session_data) if profile is None else (timer, session_data, profile) + ) + return session_id, session_data + + def _purge_session(self, session_id): + try: + timer, session_data, profile = self._sessions[session_id] + except ValueError: + timer, session_data = self._sessions[session_id] + profile = None + try: + timer.cancel() + except error.AlreadyCalled: + # if the session is time-outed, the timer has been called + pass + del self._sessions[session_id] + log.debug( + "Session {} purged{}".format( + session_id, + " (profile {})".format(profile) if profile is not None else "", + ) + ) + + def __len__(self): + return len(self._sessions) + + def __contains__(self, session_id): + return session_id in self._sessions + + def profile_get(self, session_id, profile): + try: + timer, session_data, profile_set = self._sessions[session_id] + except ValueError: + raise exceptions.InternalError( + "You need to use __getitem__ when profile is not set" + ) + except KeyError: + raise failure.Failure(KeyError(MSG_NO_SESSION)) + if profile_set != profile: + raise exceptions.InternalError("current profile differ from set profile !") + if self.resettable_timeout: + timer.reset(self.timeout) + return session_data + + def __getitem__(self, session_id): + try: + timer, session_data = self._sessions[session_id] + except ValueError: + raise exceptions.InternalError( + "You need to use profile_get instead of __getitem__ when profile is set" + ) + except KeyError: + raise failure.Failure(KeyError(MSG_NO_SESSION)) + if self.resettable_timeout: + timer.reset(self.timeout) + return session_data + + def __setitem__(self, key, value): + raise NotImplementedError("You need do use new_session to create a session") + + def __delitem__(self, session_id): + """ delete the session data """ + self._purge_session(session_id) + + def keys(self): + return list(self._sessions.keys()) + + def iterkeys(self): + return iter(self._sessions.keys()) + + +class ProfileSessions(Sessions): + """ProfileSessions extends the Sessions class, but here the profile can be + used as the key to retrieve data or delete a session (instead of session id). + """ + + def _profile_get_all_ids(self, profile): + """Return a list of the sessions ids that are associated to the given profile. + + @param profile: %(doc_profile)s + @return: a list containing the sessions ids + """ + ret = [] + for session_id in self._sessions.keys(): + try: + timer, session_data, profile_set = self._sessions[session_id] + except ValueError: + continue + if profile == profile_set: + ret.append(session_id) + return ret + + def profile_get_unique(self, profile): + """Return the data of the unique session that is associated to the given profile. + + @param profile: %(doc_profile)s + @return: + - mutable data (default: dict) of the unique session + - None if no session is associated to the profile + - raise an error if more than one session are found + """ + ids = self._profile_get_all_ids(profile) + if len(ids) > 1: + raise exceptions.InternalError( + "profile_get_unique has been used but more than one session has been found!" + ) + return ( + self.profile_get(ids[0], profile) if len(ids) == 1 else None + ) # XXX: timeout might be reset + + def profile_del_unique(self, profile): + """Delete the unique session that is associated to the given profile. + + @param profile: %(doc_profile)s + @return: None, but raise an error if more than one session are found + """ + ids = self._profile_get_all_ids(profile) + if len(ids) > 1: + raise exceptions.InternalError( + "profile_del_unique has been used but more than one session has been found!" + ) + if len(ids) == 1: + del self._sessions[ids[0]] + + +class PasswordSessions(ProfileSessions): + + # FIXME: temporary hack for the user personal key not to be lost. The session + # must actually be purged and later, when the personal key is needed, the + # profile password should be asked again in order to decrypt it. + def __init__(self, timeout=None): + ProfileSessions.__init__(self, timeout, resettable_timeout=False) + + def _purge_session(self, session_id): + log.debug( + "FIXME: PasswordSessions should ask for the profile password after the session expired" + ) + + +class Memory: + """This class manage all the persistent information""" + + def __init__(self, host): + log.info(_("Memory manager init")) + self.host = host + self._entities_cache = {} # XXX: keep presence/last resource/other data in cache + # /!\ an entity is not necessarily in roster + # main key is bare jid, value is a dict + # where main key is resource, or None for bare jid + self._key_signals = set() # key which need a signal to frontends when updated + self.subscriptions = {} + self.auth_sessions = PasswordSessions() # remember the authenticated profiles + self.disco = Discovery(host) + self.config = tools_config.parse_main_conf(log_filenames=True) + self._cache_path = Path(self.config_get("", "local_dir"), C.CACHE_DIR) + self.admins = self.config_get("", "admins_list", []) + self.admin_jids = set() + + + async def initialise(self): + self.storage = Storage() + await self.storage.initialise() + PersistentDict.storage = self.storage + self.params = Params(self.host, self.storage) + log.info(_("Loading default params template")) + self.params.load_default_params() + await self.load() + self.memory_data = PersistentDict("memory") + await self.memory_data.load() + await self.disco.load() + for admin in self.admins: + try: + admin_jid_s = await self.param_get_a_async( + "JabberID", "Connection", profile_key=admin + ) + except Exception as e: + log.warning(f"Can't retrieve jid of admin {admin!r}: {e}") + else: + if admin_jid_s is not None: + try: + admin_jid = jid.JID(admin_jid_s).userhostJID() + except RuntimeError: + log.warning(f"Invalid JID for admin {admin}: {admin_jid_s}") + else: + self.admin_jids.add(admin_jid) + + + ## Configuration ## + + def config_get(self, section, name, default=None): + """Get the main configuration option + + @param section: section of the config file (None or '' for DEFAULT) + @param name: name of the option + @param default: value to use if not found + @return: str, list or dict + """ + return tools_config.config_get(self.config, section, name, default) + + def load_xml(self, filename): + """Load parameters template from xml file + + @param filename (str): input file + @return: bool: True in case of success + """ + if not filename: + return False + filename = os.path.expanduser(filename) + if os.path.exists(filename): + try: + self.params.load_xml(filename) + log.debug(_("Parameters loaded from file: %s") % filename) + return True + except Exception as e: + log.error(_("Can't load parameters from file: %s") % e) + return False + + def save_xml(self, filename): + """Save parameters template to xml file + + @param filename (str): output file + @return: bool: True in case of success + """ + if not filename: + return False + # TODO: need to encrypt files (at least passwords !) and set permissions + filename = os.path.expanduser(filename) + try: + self.params.save_xml(filename) + log.debug(_("Parameters saved to file: %s") % filename) + return True + except Exception as e: + log.error(_("Can't save parameters to file: %s") % e) + return False + + def load(self): + """Load parameters and all memory things from db""" + # parameters data + return self.params.load_gen_params() + + def load_individual_params(self, profile): + """Load individual parameters for a profile + @param profile: %(doc_profile)s""" + return self.params.load_ind_params(profile) + + ## Profiles/Sessions management ## + + def start_session(self, password, profile): + """"Iniatialise session for a profile + + @param password(unicode): profile session password + or empty string is no password is set + @param profile: %(doc_profile)s + @raise exceptions.ProfileUnknownError if profile doesn't exists + @raise exceptions.PasswordError: the password does not match + """ + profile = self.get_profile_name(profile) + + def create_session(__): + """Called once params are loaded.""" + self._entities_cache[profile] = {} + log.info("[{}] Profile session started".format(profile)) + return False + + def backend_initialised(__): + def do_start_session(__=None): + if self.is_session_started(profile): + log.info("Session already started!") + return True + try: + # if there is a value at this point in self._entities_cache, + # it is the load_individual_params Deferred, the session is starting + session_d = self._entities_cache[profile] + except KeyError: + # else we do request the params + session_d = self._entities_cache[profile] = self.load_individual_params( + profile + ) + session_d.addCallback(create_session) + finally: + return session_d + + auth_d = defer.ensureDeferred(self.profile_authenticate(password, profile)) + auth_d.addCallback(do_start_session) + return auth_d + + if self.host.initialised.called: + return defer.succeed(None).addCallback(backend_initialised) + else: + return self.host.initialised.addCallback(backend_initialised) + + def stop_session(self, profile): + """Delete a profile session + + @param profile: %(doc_profile)s + """ + if self.host.is_connected(profile): + log.debug("Disconnecting profile because of session stop") + self.host.disconnect(profile) + self.auth_sessions.profile_del_unique(profile) + try: + self._entities_cache[profile] + except KeyError: + log.warning("Profile was not in cache") + + def _is_session_started(self, profile_key): + return self.is_session_started(self.get_profile_name(profile_key)) + + def is_session_started(self, profile): + try: + # XXX: if the value in self._entities_cache is a Deferred, + # the session is starting but not started yet + return not isinstance(self._entities_cache[profile], defer.Deferred) + except KeyError: + return False + + async def profile_authenticate(self, password, profile): + """Authenticate the profile. + + @param password (unicode): the SàT profile password + @return: None in case of success (an exception is raised otherwise) + @raise exceptions.PasswordError: the password does not match + """ + if not password and self.auth_sessions.profile_get_unique(profile): + # XXX: this allows any frontend to connect with the empty password as soon as + # the profile has been authenticated at least once before. It is OK as long as + # submitting a form with empty passwords is restricted to local frontends. + return + + sat_cipher = await self.param_get_a_async( + C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile + ) + valid = PasswordHasher.verify(password, sat_cipher) + if not valid: + log.warning(_("Authentication failure of profile {profile}").format( + profile=profile)) + raise exceptions.PasswordError("The provided profile password doesn't match.") + return await self.new_auth_session(password, profile) + + async def new_auth_session(self, key, profile): + """Start a new session for the authenticated profile. + + If there is already an existing session, no new one is created + The personal key is loaded encrypted from a PersistentDict before being decrypted. + + @param key: the key to decrypt the personal key + @param profile: %(doc_profile)s + """ + data = await PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load() + personal_key = BlockCipher.decrypt(key, data[C.MEMORY_CRYPTO_KEY]) + # Create the session for this profile and store the personal key + session_data = self.auth_sessions.profile_get_unique(profile) + if not session_data: + self.auth_sessions.new_session( + {C.MEMORY_CRYPTO_KEY: personal_key}, profile=profile + ) + log.debug("auth session created for profile %s" % profile) + + def purge_profile_session(self, profile): + """Delete cache of data of profile + @param profile: %(doc_profile)s""" + log.info(_("[%s] Profile session purge" % profile)) + self.params.purge_profile(profile) + try: + del self._entities_cache[profile] + except KeyError: + log.error( + _( + "Trying to purge roster status cache for a profile not in memory: [%s]" + ) + % profile + ) + + def get_profiles_list(self, clients=True, components=False): + """retrieve profiles list + + @param clients(bool): if True return clients profiles + @param components(bool): if True return components profiles + @return (list[unicode]): selected profiles + """ + if not clients and not components: + log.warning(_("requesting no profiles at all")) + return [] + profiles = self.storage.get_profiles_list() + if clients and components: + return sorted(profiles) + is_component = self.storage.profile_is_component + if clients: + p_filter = lambda p: not is_component(p) + else: + p_filter = lambda p: is_component(p) + + return sorted(p for p in profiles if p_filter(p)) + + def get_profile_name(self, profile_key, return_profile_keys=False): + """Return name of profile from keyword + + @param profile_key: can be the profile name or a keyword (like @DEFAULT@) + @param return_profile_keys: if True, return unmanaged profile keys (like "@ALL@"). This keys must be managed by the caller + @return: requested profile name + @raise exceptions.ProfileUnknownError if profile doesn't exists + """ + return self.params.get_profile_name(profile_key, return_profile_keys) + + def profile_set_default(self, profile): + """Set default profile + + @param profile: %(doc_profile)s + """ + # we want to be sure that the profile exists + profile = self.get_profile_name(profile) + + self.memory_data["Profile_default"] = profile + + def create_profile(self, name, password, component=None): + """Create a new profile + + @param name(unicode): profile name + @param password(unicode): profile password + Can be empty to disable password + @param component(None, unicode): set to entry point if this is a component + @return: Deferred + @raise exceptions.NotFound: component is not a known plugin import name + """ + if not name: + raise ValueError("Empty profile name") + if name[0] == "@": + raise ValueError("A profile name can't start with a '@'") + if "\n" in name: + raise ValueError("A profile name can't contain line feed ('\\n')") + + if name in self._entities_cache: + raise exceptions.ConflictError("A session for this profile exists") + + if component: + if not component in self.host.plugins: + raise exceptions.NotFound( + _( + "Can't find component {component} entry point".format( + component=component + ) + ) + ) + # FIXME: PLUGIN_INFO is not currently accessible after import, but type shoul be tested here + #  if self.host.plugins[component].PLUGIN_INFO[u"type"] != C.PLUG_TYPE_ENTRY_POINT: + #   raise ValueError(_(u"Plugin {component} is not an entry point !".format( + #   component = component))) + + d = self.params.create_profile(name, component) + + def init_personal_key(__): + # be sure to call this after checking that the profile doesn't exist yet + + # generated once for all and saved in a PersistentDict + personal_key = BlockCipher.get_random_key( + base64=True + ).decode('utf-8') + self.auth_sessions.new_session( + {C.MEMORY_CRYPTO_KEY: personal_key}, profile=name + ) # will be encrypted by param_set + + def start_fake_session(__): + # avoid ProfileNotConnected exception in param_set + self._entities_cache[name] = None + self.params.load_ind_params(name) + + def stop_fake_session(__): + del self._entities_cache[name] + self.params.purge_profile(name) + + d.addCallback(init_personal_key) + d.addCallback(start_fake_session) + d.addCallback( + lambda __: self.param_set( + C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=name + ) + ) + d.addCallback(stop_fake_session) + d.addCallback(lambda __: self.auth_sessions.profile_del_unique(name)) + return d + + def profile_delete_async(self, name, force=False): + """Delete an existing profile + + @param name: Name of the profile + @param force: force the deletion even if the profile is connected. + To be used for direct calls only (not through the bridge). + @return: a Deferred instance + """ + + def clean_memory(__): + self.auth_sessions.profile_del_unique(name) + try: + del self._entities_cache[name] + except KeyError: + pass + + d = self.params.profile_delete_async(name, force) + d.addCallback(clean_memory) + return d + + def is_component(self, profile_name): + """Tell if a profile is a component + + @param profile_name(unicode): name of the profile + @return (bool): True if profile is a component + @raise exceptions.NotFound: profile doesn't exist + """ + return self.storage.profile_is_component(profile_name) + + def get_entry_point(self, profile_name): + """Get a component entry point + + @param profile_name(unicode): name of the profile + @return (bool): True if profile is a component + @raise exceptions.NotFound: profile doesn't exist + """ + return self.storage.get_entry_point(profile_name) + + ## History ## + + def add_to_history(self, client, data): + return self.storage.add_to_history(data, client.profile) + + def _history_get_serialise(self, history_data): + return [ + (uid, timestamp, from_jid, to_jid, message, subject, mess_type, + data_format.serialise(extra)) for uid, timestamp, from_jid, to_jid, message, + subject, mess_type, extra in history_data + ] + + def _history_get(self, from_jid_s, to_jid_s, limit=C.HISTORY_LIMIT_NONE, between=True, + filters=None, profile=C.PROF_KEY_NONE): + d = self.history_get(jid.JID(from_jid_s), jid.JID(to_jid_s), limit, between, + filters, profile) + d.addCallback(self._history_get_serialise) + return d + + def history_get(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, + filters=None, profile=C.PROF_KEY_NONE): + """Retrieve messages in history + + @param from_jid (JID): source JID (full, or bare for catchall) + @param to_jid (JID): dest JID (full, or bare for catchall) + @param limit (int): maximum number of messages to get: + - 0 for no message (returns the empty list) + - C.HISTORY_LIMIT_NONE or None for unlimited + - C.HISTORY_LIMIT_DEFAULT to use the HISTORY_LIMIT parameter value + @param between (bool): confound source and dest (ignore the direction) + @param filters (dict[unicode, unicode]): pattern to filter the history results + (see bridge API for details) + @param profile (str): %(doc_profile)s + @return (D(list)): list of message data as in [message_new] + """ + assert profile != C.PROF_KEY_NONE + if limit == C.HISTORY_LIMIT_DEFAULT: + limit = int(self.param_get_a(C.HISTORY_LIMIT, "General", profile_key=profile)) + elif limit == C.HISTORY_LIMIT_NONE: + limit = None + if limit == 0: + return defer.succeed([]) + return self.storage.history_get(from_jid, to_jid, limit, between, filters, profile) + + ## Statuses ## + + def _get_presence_statuses(self, profile_key): + ret = self.presence_statuses_get(profile_key) + return {entity.full(): data for entity, data in ret.items()} + + def presence_statuses_get(self, profile_key): + """Get all the presence statuses of a profile + + @param profile_key: %(doc_profile_key)s + @return: presence data: key=entity JID, value=presence data for this entity + """ + client = self.host.get_client(profile_key) + profile_cache = self._get_profile_cache(client) + entities_presence = {} + + for entity_jid, entity_data in profile_cache.items(): + for resource, resource_data in entity_data.items(): + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.get_entity_datum(client, full_jid, "presence") + except KeyError: + continue + entities_presence.setdefault(entity_jid, {})[ + resource or "" + ] = presence_data + + return entities_presence + + def set_presence_status(self, entity_jid, show, priority, statuses, profile_key): + """Change the presence status of an entity + + @param entity_jid: jid.JID of the entity + @param show: show status + @param priority: priority + @param statuses: dictionary of statuses + @param profile_key: %(doc_profile_key)s + """ + client = self.host.get_client(profile_key) + presence_data = PresenceTuple(show, priority, statuses) + self.update_entity_data( + client, entity_jid, "presence", presence_data + ) + if entity_jid.resource and show != C.PRESENCE_UNAVAILABLE: + # If a resource is available, bare jid should not have presence information + try: + self.del_entity_datum(client, entity_jid.userhostJID(), "presence") + except (KeyError, exceptions.UnknownEntityError): + pass + + ## Resources ## + + def _get_all_resource(self, jid_s, profile_key): + client = self.host.get_client(profile_key) + jid_ = jid.JID(jid_s) + return self.get_all_resources(client, jid_) + + def get_all_resources(self, client, entity_jid): + """Return all resource from jid for which we have had data in this session + + @param entity_jid: bare jid of the entity + return (set[unicode]): set of resources + + @raise exceptions.UnknownEntityError: if entity is not in cache + @raise ValueError: entity_jid has a resource + """ + # FIXME: is there a need to keep cache data for resources which are not connected anymore? + if entity_jid.resource: + raise ValueError( + "get_all_resources must be used with a bare jid (got {})".format(entity_jid) + ) + profile_cache = self._get_profile_cache(client) + try: + entity_data = profile_cache[entity_jid.userhostJID()] + except KeyError: + raise exceptions.UnknownEntityError( + "Entity {} not in cache".format(entity_jid) + ) + resources = set(entity_data.keys()) + resources.discard(None) + return resources + + def get_available_resources(self, client, entity_jid): + """Return available resource for entity_jid + + This method differs from get_all_resources by returning only available resources + @param entity_jid: bare jid of the entit + return (list[unicode]): list of available resources + + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + available = [] + for resource in self.get_all_resources(client, entity_jid): + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.get_entity_datum(client, full_jid, "presence") + except KeyError: + log.debug("Can't get presence data for {}".format(full_jid)) + else: + if presence_data.show != C.PRESENCE_UNAVAILABLE: + available.append(resource) + return available + + def _get_main_resource(self, jid_s, profile_key): + client = self.host.get_client(profile_key) + jid_ = jid.JID(jid_s) + return self.main_resource_get(client, jid_) or "" + + def main_resource_get(self, client, entity_jid): + """Return the main resource used by an entity + + @param entity_jid: bare entity jid + @return (unicode): main resource or None + """ + if entity_jid.resource: + raise ValueError( + "main_resource_get must be used with a bare jid (got {})".format(entity_jid) + ) + try: + if self.host.plugins["XEP-0045"].is_joined_room(client, entity_jid): + return None # MUC rooms have no main resource + except KeyError: # plugin not found + pass + try: + resources = self.get_all_resources(client, entity_jid) + except exceptions.UnknownEntityError: + log.warning("Entity is not in cache, we can't find any resource") + return None + priority_resources = [] + for resource in resources: + full_jid = copy.copy(entity_jid) + full_jid.resource = resource + try: + presence_data = self.get_entity_datum(client, full_jid, "presence") + except KeyError: + log.debug("No presence information for {}".format(full_jid)) + continue + priority_resources.append((resource, presence_data.priority)) + try: + return max(priority_resources, key=lambda res_tuple: res_tuple[1])[0] + except ValueError: + log.warning("No resource found at all for {}".format(entity_jid)) + return None + + ## Entities data ## + + def _get_profile_cache(self, client): + """Check profile validity and return its cache + + @param client: SatXMPPClient + @return (dict): profile cache + """ + return self._entities_cache[client.profile] + + def set_signal_on_update(self, key, signal=True): + """Set a signal flag on the key + + When the key will be updated, a signal will be sent to frontends + @param key: key to signal + @param signal(boolean): if True, do the signal + """ + if signal: + self._key_signals.add(key) + else: + self._key_signals.discard(key) + + def get_all_entities_iter(self, client, with_bare=False): + """Return an iterator of full jids of all entities in cache + + @param with_bare: if True, include bare jids + @return (list[unicode]): list of jids + """ + profile_cache = self._get_profile_cache(client) + # we construct a list of all known full jids (bare jid of entities x resources) + for bare_jid, entity_data in profile_cache.items(): + for resource in entity_data.keys(): + if resource is None: + continue + full_jid = copy.copy(bare_jid) + full_jid.resource = resource + yield full_jid + + def update_entity_data( + self, client, entity_jid, key, value, silent=False + ): + """Set a misc data for an entity + + If key was registered with set_signal_on_update, a signal will be sent to frontends + @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of + all entities, C.ENTITY_ALL for all entities (all resources + bare jids) + @param key: key to set (eg: C.ENTITY_TYPE) + @param value: value for this key (eg: C.ENTITY_TYPE_MUC) + @param silent(bool): if True, doesn't send signal to frontend, even if there is a + signal flag (see set_signal_on_update) + """ + profile_cache = self._get_profile_cache(client) + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + entities = self.get_all_entities_iter(client, entity_jid == C.ENTITY_ALL) + else: + entities = (entity_jid,) + + for jid_ in entities: + entity_data = profile_cache.setdefault(jid_.userhostJID(), {}).setdefault( + jid_.resource, {} + ) + + entity_data[key] = value + if key in self._key_signals and not silent: + self.host.bridge.entity_data_updated( + jid_.full(), + key, + data_format.serialise(value), + client.profile + ) + + def del_entity_datum(self, client, entity_jid, key): + """Delete a data for an entity + + @param entity_jid: JID of the entity, C.ENTITY_ALL_RESOURCES for all resources of all entities, + C.ENTITY_ALL for all entities (all resources + bare jids) + @param key: key to delete (eg: C.ENTITY_TYPE) + + @raise exceptions.UnknownEntityError: if entity is not in cache + @raise KeyError: key is not in cache + """ + profile_cache = self._get_profile_cache(client) + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + entities = self.get_all_entities_iter(client, entity_jid == C.ENTITY_ALL) + else: + entities = (entity_jid,) + + for jid_ in entities: + try: + entity_data = profile_cache[jid_.userhostJID()][jid_.resource] + except KeyError: + raise exceptions.UnknownEntityError( + "Entity {} not in cache".format(jid_) + ) + try: + del entity_data[key] + except KeyError as e: + if entity_jid in (C.ENTITY_ALL_RESOURCES, C.ENTITY_ALL): + continue # we ignore KeyError when deleting keys from several entities + else: + raise e + + def _get_entities_data(self, entities_jids, keys_list, profile_key): + client = self.host.get_client(profile_key) + ret = self.entities_data_get( + client, [jid.JID(jid_) for jid_ in entities_jids], keys_list + ) + return { + jid_.full(): {k: data_format.serialise(v) for k,v in data.items()} + for jid_, data in ret.items() + } + + def entities_data_get(self, client, entities_jids, keys_list=None): + """Get a list of cached values for several entities at once + + @param entities_jids: jids of the entities, or empty list for all entities in cache + @param keys_list (iterable,None): list of keys to get, None for everything + @param profile_key: %(doc_profile_key)s + @return: dict withs values for each key in keys_list. + if there is no value of a given key, resulting dict will + have nothing with that key nether + if an entity doesn't exist in cache, it will not appear + in resulting dict + + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + + def fill_entity_data(entity_cache_data): + entity_data = {} + if keys_list is None: + entity_data = entity_cache_data + else: + for key in keys_list: + try: + entity_data[key] = entity_cache_data[key] + except KeyError: + continue + return entity_data + + profile_cache = self._get_profile_cache(client) + ret_data = {} + if entities_jids: + for entity in entities_jids: + try: + entity_cache_data = profile_cache[entity.userhostJID()][ + entity.resource + ] + except KeyError: + continue + ret_data[entity.full()] = fill_entity_data(entity_cache_data, keys_list) + else: + for bare_jid, data in profile_cache.items(): + for resource, entity_cache_data in data.items(): + full_jid = copy.copy(bare_jid) + full_jid.resource = resource + ret_data[full_jid] = fill_entity_data(entity_cache_data) + + return ret_data + + def _get_entity_data(self, entity_jid_s, keys_list=None, profile=C.PROF_KEY_NONE): + return self.entity_data_get( + self.host.get_client(profile), jid.JID(entity_jid_s), keys_list) + + def entity_data_get(self, client, entity_jid, keys_list=None): + """Get a list of cached values for entity + + @param entity_jid: JID of the entity + @param keys_list (iterable,None): list of keys to get, None for everything + @param profile_key: %(doc_profile_key)s + @return: dict withs values for each key in keys_list. + if there is no value of a given key, resulting dict will + have nothing with that key nether + + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + profile_cache = self._get_profile_cache(client) + try: + entity_data = profile_cache[entity_jid.userhostJID()][entity_jid.resource] + except KeyError: + raise exceptions.UnknownEntityError( + "Entity {} not in cache (was requesting {})".format( + entity_jid, keys_list + ) + ) + if keys_list is None: + return entity_data + + return {key: entity_data[key] for key in keys_list if key in entity_data} + + def get_entity_datum(self, client, entity_jid, key): + """Get a datum from entity + + @param entity_jid: JID of the entity + @param key: key to get + @return: requested value + + @raise exceptions.UnknownEntityError: if entity is not in cache + @raise KeyError: if there is no value for this key and this entity + """ + return self.entity_data_get(client, entity_jid, (key,))[key] + + def del_entity_cache( + self, entity_jid, delete_all_resources=True, profile_key=C.PROF_KEY_NONE + ): + """Remove all cached data for entity + + @param entity_jid: JID of the entity to delete + @param delete_all_resources: if True also delete all known resources from cache (a bare jid must be given in this case) + @param profile_key: %(doc_profile_key)s + + @raise exceptions.UnknownEntityError: if entity is not in cache + """ + client = self.host.get_client(profile_key) + profile_cache = self._get_profile_cache(client) + + if delete_all_resources: + if entity_jid.resource: + raise ValueError(_("Need a bare jid to delete all resources")) + try: + del profile_cache[entity_jid] + except KeyError: + raise exceptions.UnknownEntityError( + "Entity {} not in cache".format(entity_jid) + ) + else: + try: + del profile_cache[entity_jid.userhostJID()][entity_jid.resource] + except KeyError: + raise exceptions.UnknownEntityError( + "Entity {} not in cache".format(entity_jid) + ) + + ## Encryption ## + + def encrypt_value(self, value, profile): + """Encrypt a value for the given profile. The personal key must be loaded + already in the profile session, that should be the case if the profile is + already authenticated. + + @param value (str): the value to encrypt + @param profile (str): %(doc_profile)s + @return: the deferred encrypted value + """ + try: + personal_key = self.auth_sessions.profile_get_unique(profile)[ + C.MEMORY_CRYPTO_KEY + ] + except TypeError: + raise exceptions.InternalError( + _("Trying to encrypt a value for %s while the personal key is undefined!") + % profile + ) + return BlockCipher.encrypt(personal_key, value) + + def decrypt_value(self, value, profile): + """Decrypt a value for the given profile. The personal key must be loaded + already in the profile session, that should be the case if the profile is + already authenticated. + + @param value (str): the value to decrypt + @param profile (str): %(doc_profile)s + @return: the deferred decrypted value + """ + try: + personal_key = self.auth_sessions.profile_get_unique(profile)[ + C.MEMORY_CRYPTO_KEY + ] + except TypeError: + raise exceptions.InternalError( + _("Trying to decrypt a value for %s while the personal key is undefined!") + % profile + ) + return BlockCipher.decrypt(personal_key, value) + + def encrypt_personal_data(self, data_key, data_value, crypto_key, profile): + """Re-encrypt a personal data (saved to a PersistentDict). + + @param data_key: key for the individual PersistentDict instance + @param data_value: the value to be encrypted + @param crypto_key: the key to encrypt the value + @param profile: %(profile_doc)s + @return: a deferred None value + """ + + def got_ind_memory(data): + data[data_key] = BlockCipher.encrypt(crypto_key, data_value) + return data.force(data_key) + + def done(__): + log.debug( + _("Personal data (%(ns)s, %(key)s) has been successfuly encrypted") + % {"ns": C.MEMORY_CRYPTO_NAMESPACE, "key": data_key} + ) + + d = PersistentDict(C.MEMORY_CRYPTO_NAMESPACE, profile).load() + return d.addCallback(got_ind_memory).addCallback(done) + + ## Subscription requests ## + + def add_waiting_sub(self, type_, entity_jid, profile_key): + """Called when a subcription request is received""" + profile = self.get_profile_name(profile_key) + assert profile + if profile not in self.subscriptions: + self.subscriptions[profile] = {} + self.subscriptions[profile][entity_jid] = type_ + + def del_waiting_sub(self, entity_jid, profile_key): + """Called when a subcription request is finished""" + profile = self.get_profile_name(profile_key) + assert profile + if profile in self.subscriptions and entity_jid in self.subscriptions[profile]: + del self.subscriptions[profile][entity_jid] + + def sub_waiting_get(self, profile_key): + """Called to get a list of currently waiting subscription requests""" + profile = self.get_profile_name(profile_key) + if not profile: + log.error(_("Asking waiting subscriptions for a non-existant profile")) + return {} + if profile not in self.subscriptions: + return {} + + return self.subscriptions[profile] + + ## Parameters ## + + def get_string_param_a(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): + return self.params.get_string_param_a(name, category, attr, profile_key) + + def param_get_a(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): + return self.params.param_get_a(name, category, attr, profile_key=profile_key) + + def param_get_a_async( + self, + name, + category, + attr="value", + security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE, + ): + return self.params.param_get_a_async( + name, category, attr, security_limit, profile_key + ) + + def _get_params_values_from_category( + self, category, security_limit, app, extra_s, profile_key + ): + return self.params._get_params_values_from_category( + category, security_limit, app, extra_s, profile_key + ) + + def async_get_string_param_a( + self, name, category, attribute="value", security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE): + + profile = self.get_profile_name(profile_key) + return defer.ensureDeferred(self.params.async_get_string_param_a( + name, category, attribute, security_limit, profile + )) + + def _get_params_ui(self, security_limit, app, extra_s, profile_key): + return self.params._get_params_ui(security_limit, app, extra_s, profile_key) + + def params_categories_get(self): + return self.params.params_categories_get() + + def param_set( + self, + name, + value, + category, + security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE, + ): + return self.params.param_set(name, value, category, security_limit, profile_key) + + def update_params(self, xml): + return self.params.update_params(xml) + + def params_register_app(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=""): + return self.params.params_register_app(xml, security_limit, app) + + def set_default(self, name, category, callback, errback=None): + return self.params.set_default(name, category, callback, errback) + + ## Private Data ## + + def _private_data_set(self, namespace, key, data_s, profile_key): + client = self.host.get_client(profile_key) + # we accept any type + data = data_format.deserialise(data_s, type_check=None) + return defer.ensureDeferred(self.storage.set_private_value( + namespace, key, data, binary=True, profile=client.profile)) + + def _private_data_get(self, namespace, key, profile_key): + client = self.host.get_client(profile_key) + d = defer.ensureDeferred( + self.storage.get_privates( + namespace, [key], binary=True, profile=client.profile) + ) + d.addCallback(lambda data_dict: data_format.serialise(data_dict.get(key))) + return d + + def _private_data_delete(self, namespace, key, profile_key): + client = self.host.get_client(profile_key) + return defer.ensureDeferred(self.storage.del_private_value( + namespace, key, binary=True, profile=client.profile)) + + ## Files ## + + def check_file_permission( + self, + file_data: dict, + peer_jid: Optional[jid.JID], + perms_to_check: Optional[Tuple[str]], + set_affiliation: bool = False + ) -> None: + """Check that an entity has the right permission on a file + + @param file_data: data of one file, as returned by get_files + @param peer_jid: entity trying to access the file + @param perms_to_check: permissions to check + tuple of C.ACCESS_PERM_* + @param check_parents: if True, also check all parents until root node + @parma set_affiliation: if True, "affiliation" metadata will be set + @raise exceptions.PermissionError: peer_jid doesn't have all permission + in perms_to_check for file_data + @raise exceptions.InternalError: perms_to_check is invalid + """ + # TODO: knowing if user is owner is not enough, we need to check permission + # to see if user can modify/delete files, and set corresponding affiliation (publisher, member) + if peer_jid is None and perms_to_check is None: + return + peer_jid = peer_jid.userhostJID() + if peer_jid == file_data["owner"]: + if set_affiliation: + file_data['affiliation'] = 'owner' + # the owner has all rights, nothing to check + return + if not C.ACCESS_PERMS.issuperset(perms_to_check): + raise exceptions.InternalError(_("invalid permission")) + + for perm in perms_to_check: + # we check each perm and raise PermissionError as soon as one condition is not valid + # we must never return here, we only return after the loop if nothing was blocking the access + try: + perm_data = file_data["access"][perm] + perm_type = perm_data["type"] + except KeyError: + # No permission is set. + # If we are in a root file/directory, we deny access + # otherwise, we use public permission, as the parent directory will + # block anyway, this avoid to have to recursively change permissions for + # all sub directories/files when modifying a permission + if not file_data.get('parent'): + raise exceptions.PermissionError() + else: + perm_type = C.ACCESS_TYPE_PUBLIC + if perm_type == C.ACCESS_TYPE_PUBLIC: + continue + elif perm_type == C.ACCESS_TYPE_WHITELIST: + try: + jids = perm_data["jids"] + except KeyError: + raise exceptions.PermissionError() + if peer_jid.full() in jids: + continue + else: + raise exceptions.PermissionError() + else: + raise exceptions.InternalError( + _("unknown access type: {type}").format(type=perm_type) + ) + + async def check_permission_to_root(self, client, file_data, peer_jid, perms_to_check): + """do check_file_permission on file_data and all its parents until root""" + current = file_data + while True: + self.check_file_permission(current, peer_jid, perms_to_check) + parent = current["parent"] + if not parent: + break + files_data = await self.get_files( + client, peer_jid=None, file_id=parent, perms_to_check=None + ) + try: + current = files_data[0] + except IndexError: + raise exceptions.DataError("Missing parent") + + async def _get_parent_dir( + self, client, path, parent, namespace, owner, peer_jid, perms_to_check + ): + """Retrieve parent node from a path, or last existing directory + + each directory of the path will be retrieved, until the last existing one + @return (tuple[unicode, list[unicode])): parent, remaining path elements: + - parent is the id of the last retrieved directory (or u'' for root) + - remaining path elements are the directories which have not been retrieved + (i.e. which don't exist) + """ + # if path is set, we have to retrieve parent directory of the file(s) from it + if parent is not None: + raise exceptions.ConflictError( + _("You can't use path and parent at the same time") + ) + path_elts = [_f for _f in path.split("/") if _f] + if {"..", "."}.intersection(path_elts): + raise ValueError(_('".." or "." can\'t be used in path')) + + # we retrieve all directories from path until we get the parent container + # non existing directories will be created + parent = "" + for idx, path_elt in enumerate(path_elts): + directories = await self.storage.get_files( + client, + parent=parent, + type_=C.FILE_TYPE_DIRECTORY, + name=path_elt, + namespace=namespace, + owner=owner, + ) + if not directories: + return (parent, path_elts[idx:]) + # from this point, directories don't exist anymore, we have to create them + elif len(directories) > 1: + raise exceptions.InternalError( + _("Several directories found, this should not happen") + ) + else: + directory = directories[0] + self.check_file_permission(directory, peer_jid, perms_to_check) + parent = directory["id"] + return (parent, []) + + def get_file_affiliations(self, file_data: dict) -> Dict[jid.JID, str]: + """Convert file access to pubsub like affiliations""" + affiliations = {} + access_data = file_data['access'] + + read_data = access_data.get(C.ACCESS_PERM_READ, {}) + if read_data.get('type') == C.ACCESS_TYPE_WHITELIST: + for entity_jid_s in read_data['jids']: + entity_jid = jid.JID(entity_jid_s) + affiliations[entity_jid] = 'member' + + write_data = access_data.get(C.ACCESS_PERM_WRITE, {}) + if write_data.get('type') == C.ACCESS_TYPE_WHITELIST: + for entity_jid_s in write_data['jids']: + entity_jid = jid.JID(entity_jid_s) + affiliations[entity_jid] = 'publisher' + + owner = file_data.get('owner') + if owner: + affiliations[owner] = 'owner' + + return affiliations + + def _set_file_affiliations_update( + self, + access: dict, + file_data: dict, + affiliations: Dict[jid.JID, str] + ) -> None: + read_data = access.setdefault(C.ACCESS_PERM_READ, {}) + if read_data.get('type') != C.ACCESS_TYPE_WHITELIST: + read_data['type'] = C.ACCESS_TYPE_WHITELIST + if 'jids' not in read_data: + read_data['jids'] = [] + read_whitelist = read_data['jids'] + write_data = access.setdefault(C.ACCESS_PERM_WRITE, {}) + if write_data.get('type') != C.ACCESS_TYPE_WHITELIST: + write_data['type'] = C.ACCESS_TYPE_WHITELIST + if 'jids' not in write_data: + write_data['jids'] = [] + write_whitelist = write_data['jids'] + for entity_jid, affiliation in affiliations.items(): + entity_jid_s = entity_jid.full() + if affiliation == "none": + try: + read_whitelist.remove(entity_jid_s) + except ValueError: + log.warning( + "removing affiliation from an entity without read permission: " + f"{entity_jid}" + ) + try: + write_whitelist.remove(entity_jid_s) + except ValueError: + pass + elif affiliation == "publisher": + if entity_jid_s not in read_whitelist: + read_whitelist.append(entity_jid_s) + if entity_jid_s not in write_whitelist: + write_whitelist.append(entity_jid_s) + elif affiliation == "member": + if entity_jid_s not in read_whitelist: + read_whitelist.append(entity_jid_s) + try: + write_whitelist.remove(entity_jid_s) + except ValueError: + pass + elif affiliation == "owner": + raise NotImplementedError('"owner" affiliation can\'t be set') + else: + raise ValueError(f"unknown affiliation: {affiliation!r}") + + async def set_file_affiliations( + self, + client, + file_data: dict, + affiliations: Dict[jid.JID, str] + ) -> None: + """Apply pubsub like affiliation to file_data + + Affiliations are converted to access types, then set in a whitelist. + Affiliation are mapped as follow: + - "owner" can't be set (for now) + - "publisher" gives read and write permissions + - "member" gives read permission only + - "none" removes both read and write permissions + """ + file_id = file_data['id'] + await self.file_update( + file_id, + 'access', + update_cb=partial( + self._set_file_affiliations_update, + file_data=file_data, + affiliations=affiliations + ), + ) + + def _set_file_access_model_update( + self, + access: dict, + file_data: dict, + access_model: str + ) -> None: + read_data = access.setdefault(C.ACCESS_PERM_READ, {}) + if access_model == "open": + requested_type = C.ACCESS_TYPE_PUBLIC + elif access_model == "whitelist": + requested_type = C.ACCESS_TYPE_WHITELIST + else: + raise ValueError(f"unknown access model: {access_model}") + + read_data['type'] = requested_type + if requested_type == C.ACCESS_TYPE_WHITELIST and 'jids' not in read_data: + read_data['jids'] = [] + + async def set_file_access_model( + self, + client, + file_data: dict, + access_model: str, + ) -> None: + """Apply pubsub like access_model to file_data + + Only 2 access models are supported so far: + - "open": set public access to file/dir + - "whitelist": set whitelist to file/dir + """ + file_id = file_data['id'] + await self.file_update( + file_id, + 'access', + update_cb=partial( + self._set_file_access_model_update, + file_data=file_data, + access_model=access_model + ), + ) + + def get_files_owner( + self, + client, + owner: Optional[jid.JID], + peer_jid: Optional[jid.JID], + file_id: Optional[str] = None, + parent: Optional[str] = None + ) -> jid.JID: + """Get owner to use for a file operation + + if owner is not explicitely set, a suitable one will be used (client.jid for + clients, peer_jid for components). + @raise exception.InternalError: we are one a component, and neither owner nor + peer_jid are set + """ + if owner is not None: + return owner.userhostJID() + if client is None: + # client may be None when looking for file with public_id + return None + if file_id or parent: + # owner has already been filtered on parent file + return None + if not client.is_component: + return client.jid.userhostJID() + if peer_jid is None: + raise exceptions.InternalError( + "Owner must be set for component if peer_jid is None" + ) + return peer_jid.userhostJID() + + async def get_files( + self, client, peer_jid, file_id=None, version=None, parent=None, path=None, + type_=None, file_hash=None, hash_algo=None, name=None, namespace=None, + mime_type=None, public_id=None, owner=None, access=None, projection=None, + unique=False, perms_to_check=(C.ACCESS_PERM_READ,)): + """Retrieve files with with given filters + + @param peer_jid(jid.JID, None): jid trying to access the file + needed to check permission. + Use None to ignore permission (perms_to_check must be None too) + @param file_id(unicode, None): id of the file + None to ignore + @param version(unicode, None): version of the file + None to ignore + empty string to look for current version + @param parent(unicode, None): id of the directory containing the files + None to ignore + empty string to look for root files/directories + @param path(Path, unicode, None): path to the directory containing the files + @param type_(unicode, None): type of file filter, can be one of C.FILE_TYPE_* + @param file_hash(unicode, None): hash of the file to retrieve + @param hash_algo(unicode, None): algorithm use for file_hash + @param name(unicode, None): name of the file to retrieve + @param namespace(unicode, None): namespace of the files to retrieve + @param mime_type(unicode, None): filter on this mime type + @param public_id(unicode, None): filter on this public id + @param owner(jid.JID, None): if not None, only get files from this owner + @param access(dict, None): get file with given access (see [set_file]) + @param projection(list[unicode], None): name of columns to retrieve + None to retrieve all + @param unique(bool): if True will remove duplicates + @param perms_to_check(tuple[unicode],None): permission to check + must be a tuple of C.ACCESS_PERM_* or None + if None, permission will no be checked (peer_jid must be None too in this + case) + other params are the same as for [set_file] + @return (list[dict]): files corresponding to filters + @raise exceptions.NotFound: parent directory not found (when path is specified) + @raise exceptions.PermissionError: peer_jid can't use perms_to_check for one of + the file + on the path + """ + if peer_jid is None and perms_to_check or perms_to_check is None and peer_jid: + raise exceptions.InternalError( + "if you want to disable permission check, both peer_jid and " + "perms_to_check must be None" + ) + owner = self.get_files_owner(client, owner, peer_jid, file_id, parent) + if path is not None: + path = str(path) + # permission are checked by _get_parent_dir + parent, remaining_path_elts = await self._get_parent_dir( + client, path, parent, namespace, owner, peer_jid, perms_to_check + ) + if remaining_path_elts: + # if we have remaining path elements, + # the parent directory is not found + raise failure.Failure(exceptions.NotFound()) + if parent and peer_jid: + # if parent is given directly and permission check is requested, + # we need to check all the parents + parent_data = await self.storage.get_files(client, file_id=parent) + try: + parent_data = parent_data[0] + except IndexError: + raise exceptions.DataError("mising parent") + await self.check_permission_to_root( + client, parent_data, peer_jid, perms_to_check + ) + + files = await self.storage.get_files( + client, + file_id=file_id, + version=version, + parent=parent, + type_=type_, + file_hash=file_hash, + hash_algo=hash_algo, + name=name, + namespace=namespace, + mime_type=mime_type, + public_id=public_id, + owner=owner, + access=access, + projection=projection, + unique=unique, + ) + + if peer_jid: + # if permission are checked, we must remove all file that user can't access + to_remove = [] + for file_data in files: + try: + self.check_file_permission( + file_data, peer_jid, perms_to_check, set_affiliation=True + ) + except exceptions.PermissionError: + to_remove.append(file_data) + for file_data in to_remove: + files.remove(file_data) + return files + + async def set_file( + self, client, name, file_id=None, version="", parent=None, path=None, + type_=C.FILE_TYPE_FILE, file_hash=None, hash_algo=None, size=None, + namespace=None, mime_type=None, public_id=None, created=None, modified=None, + owner=None, access=None, extra=None, peer_jid=None, + perms_to_check=(C.ACCESS_PERM_WRITE,) + ): + """Set a file metadata + + @param name(unicode): basename of the file + @param file_id(unicode): unique id of the file + @param version(unicode): version of this file + empty string for current version or when there is no versioning + @param parent(unicode, None): id of the directory containing the files + @param path(unicode, None): virtual path of the file in the namespace + if set, parent must be None. All intermediate directories will be created + if needed, using current access. + @param type_(str, None): type of file filter, can be one of C.FILE_TYPE_* + @param file_hash(unicode): unique hash of the payload + @param hash_algo(unicode): algorithm used for hashing the file (usually sha-256) + @param size(int): size in bytes + @param namespace(unicode, None): identifier (human readable is better) to group + files + For instance, namespace could be used to group files in a specific photo album + @param mime_type(unicode): MIME type of the file, or None if not known/guessed + @param public_id(unicode): id used to share publicly the file via HTTP + @param created(int): UNIX time of creation + @param modified(int,None): UNIX time of last modification, or None to use + created date + @param owner(jid.JID, None): jid of the owner of the file (mainly useful for + component) + will be used to check permission (only bare jid is used, don't use with MUC). + Use None to ignore permission (perms_to_check must be None too) + @param access(dict, None): serialisable dictionary with access rules. + None (or empty dict) to use private access, i.e. allow only profile's jid to + access the file + key can be on on C.ACCESS_PERM_*, + then a sub dictionary with a type key is used (one of C.ACCESS_TYPE_*). + According to type, extra keys can be used: + - C.ACCESS_TYPE_PUBLIC: the permission is granted for everybody + - C.ACCESS_TYPE_WHITELIST: the permission is granted for jids (as unicode) + in the 'jids' key + will be encoded to json in database + @param extra(dict, None): serialisable dictionary of any extra data + will be encoded to json in database + @param perms_to_check(tuple[unicode],None): permission to check + must be a tuple of C.ACCESS_PERM_* or None + if None, permission will not be checked (peer_jid must be None too in this + case) + @param profile(unicode): profile owning the file + """ + if "/" in name: + raise ValueError('name must not contain a slash ("/")') + if file_id is None: + file_id = shortuuid.uuid() + if ( + file_hash is not None + and hash_algo is None + or hash_algo is not None + and file_hash is None + ): + raise ValueError("file_hash and hash_algo must be set at the same time") + if mime_type is None: + mime_type, __ = mimetypes.guess_type(name) + else: + mime_type = mime_type.lower() + if public_id is not None: + assert len(public_id)>0 + if created is None: + created = time.time() + if namespace is not None: + namespace = namespace.strip() or None + if type_ == C.FILE_TYPE_DIRECTORY: + if any((version, file_hash, size, mime_type)): + raise ValueError( + "version, file_hash, size and mime_type can't be set for a directory" + ) + owner = self.get_files_owner(client, owner, peer_jid, file_id, parent) + + if path is not None: + path = str(path) + # _get_parent_dir will check permissions if peer_jid is set, so we use owner + parent, remaining_path_elts = await self._get_parent_dir( + client, path, parent, namespace, owner, owner, perms_to_check + ) + # if remaining directories don't exist, we have to create them + for new_dir in remaining_path_elts: + new_dir_id = shortuuid.uuid() + await self.storage.set_file( + client, + name=new_dir, + file_id=new_dir_id, + version="", + parent=parent, + type_=C.FILE_TYPE_DIRECTORY, + namespace=namespace, + created=time.time(), + owner=owner, + access=access, + extra={}, + ) + parent = new_dir_id + elif parent is None: + parent = "" + + await self.storage.set_file( + client, + file_id=file_id, + version=version, + parent=parent, + type_=type_, + file_hash=file_hash, + hash_algo=hash_algo, + name=name, + size=size, + namespace=namespace, + mime_type=mime_type, + public_id=public_id, + created=created, + modified=modified, + owner=owner, + access=access, + extra=extra, + ) + + async def file_get_used_space( + self, + client, + peer_jid: jid.JID, + owner: Optional[jid.JID] = None + ) -> int: + """Get space taken by all files owned by an entity + + @param peer_jid: entity requesting the size + @param owner: entity owning the file to check. If None, will be determined by + get_files_owner + @return: size of total space used by files of this owner + """ + owner = self.get_files_owner(client, owner, peer_jid) + if peer_jid.userhostJID() != owner and client.profile not in self.admins: + raise exceptions.PermissionError("You are not allowed to check this size") + return await self.storage.file_get_used_space(client, owner) + + def file_update(self, file_id, column, update_cb): + """Update a file column taking care of race condition + + access is NOT checked in this method, it must be checked beforehand + @param file_id(unicode): id of the file to update + @param column(unicode): one of "access" or "extra" + @param update_cb(callable): method to update the value of the colum + the method will take older value as argument, and must update it in place + Note that the callable must be thread-safe + """ + return self.storage.file_update(file_id, column, update_cb) + + @defer.inlineCallbacks + def _delete_file( + self, + client, + peer_jid: jid.JID, + recursive: bool, + files_path: Path, + file_data: dict + ): + """Internal method to delete files/directories recursively + + @param peer_jid(jid.JID): entity requesting the deletion (must be owner of files + to delete) + @param recursive(boolean): True if recursive deletion is needed + @param files_path(unicode): path of the directory containing the actual files + @param file_data(dict): data of the file to delete + """ + if file_data['owner'] != peer_jid: + raise exceptions.PermissionError( + "file {file_name} can't be deleted, {peer_jid} is not the owner" + .format(file_name=file_data['name'], peer_jid=peer_jid.full())) + if file_data['type'] == C.FILE_TYPE_DIRECTORY: + sub_files = yield self.get_files(client, peer_jid, parent=file_data['id']) + if sub_files and not recursive: + raise exceptions.DataError(_("Can't delete directory, it is not empty")) + # we first delete the sub-files + for sub_file_data in sub_files: + if sub_file_data['type'] == C.FILE_TYPE_DIRECTORY: + sub_file_path = files_path / sub_file_data['name'] + else: + sub_file_path = files_path + yield self._delete_file( + client, peer_jid, recursive, sub_file_path, sub_file_data) + # then the directory itself + yield self.storage.file_delete(file_data['id']) + elif file_data['type'] == C.FILE_TYPE_FILE: + log.info(_("deleting file {name} with hash {file_hash}").format( + name=file_data['name'], file_hash=file_data['file_hash'])) + yield self.storage.file_delete(file_data['id']) + references = yield self.get_files( + client, peer_jid, file_hash=file_data['file_hash']) + if references: + log.debug("there are still references to the file, we keep it") + else: + file_path = os.path.join(files_path, file_data['file_hash']) + log.info(_("no reference left to {file_path}, deleting").format( + file_path=file_path)) + try: + os.unlink(file_path) + except FileNotFoundError: + log.error(f"file at {file_path!r} doesn't exist but it was referenced in files database") + else: + raise exceptions.InternalError('Unexpected file type: {file_type}' + .format(file_type=file_data['type'])) + + async def file_delete(self, client, peer_jid, file_id, recursive=False): + """Delete a single file or a directory and all its sub-files + + @param file_id(unicode): id of the file to delete + @param peer_jid(jid.JID): entity requesting the deletion, + must be owner of all files to delete + @param recursive(boolean): must be True to delete a directory and all sub-files + """ + # FIXME: we only allow owner of file to delete files for now, but WRITE access + # should be checked too + files_data = await self.get_files(client, peer_jid, file_id) + if not files_data: + raise exceptions.NotFound("Can't find the file with id {file_id}".format( + file_id=file_id)) + file_data = files_data[0] + if file_data["type"] != C.FILE_TYPE_DIRECTORY and recursive: + raise ValueError("recursive can only be set for directories") + files_path = self.host.get_local_path(None, C.FILES_DIR) + await self._delete_file(client, peer_jid, recursive, files_path, file_data) + + ## Cache ## + + def get_cache_path(self, namespace: str, *args: str) -> Path: + """Get path to use to get a common path for a namespace + + This can be used by plugins to manage permanent data. It's the responsability + of plugins to clean this directory from unused data. + @param namespace: unique namespace to use + @param args: extra identifier which will be added to the path + """ + namespace = namespace.strip().lower() + return Path( + self._cache_path, + regex.path_escape(namespace), + *(regex.path_escape(a) for a in args) + ) + + ## Misc ## + + def is_entity_available(self, client, entity_jid): + """Tell from the presence information if the given entity is available. + + @param entity_jid (JID): the entity to check (if bare jid is used, all resources are tested) + @return (bool): True if entity is available + """ + if not entity_jid.resource: + return bool( + self.get_available_resources(client, entity_jid) + ) # is any resource is available, entity is available + try: + presence_data = self.get_entity_datum(client, entity_jid, "presence") + except KeyError: + log.debug("No presence information for {}".format(entity_jid)) + return False + return presence_data.show != C.PRESENCE_UNAVAILABLE + + def is_admin(self, profile: str) -> bool: + """Tell if given profile has administrator privileges""" + return profile in self.admins + + def is_admin_jid(self, entity: jid.JID) -> bool: + """Tells if an entity jid correspond to an admin one + + It is sometime not possible to use the profile alone to check if an entity is an + admin (e.g. a request managed by a component). In this case we check if the JID + correspond to an admin profile + """ + return entity.userhostJID() in self.admin_jids diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/README Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,3 @@ +This directory and subdirectories contains Alembic migration scripts. + +Please check Libervia documentation for details. diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/alembic.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/alembic.ini Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,89 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = %(here)s + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +# prepend_sys_path = . + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to migration/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat migration/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/env.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/env.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,93 @@ +import asyncio +from logging.config import fileConfig +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import create_async_engine +from alembic import context +from libervia.backend.memory import sqla_config +from libervia.backend.memory.sqla_mapping import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + db_config = sqla_config.get_db_config() + context.configure( + url=db_config["url"], + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def include_name(name, type_, parent_names): + if type_ == "table": + if name.startswith("pubsub_items_fts"): + return False + return True + + +def do_run_migrations(connection): + context.configure( + connection=connection, + target_metadata=target_metadata, + render_as_batch=True, + include_name=include_name + ) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + db_config = sqla_config.get_db_config() + engine = create_async_engine( + db_config["url"], + poolclass=pool.NullPool, + future=True, + ) + + async with engine.connect() as connection: + await connection.run_sync(do_run_migrations) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + asyncio.run(run_migrations_online()) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/script.py.mako --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/script.py.mako Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/129ac51807e4_create_virtual_table_for_full_text_.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/versions/129ac51807e4_create_virtual_table_for_full_text_.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,49 @@ +"""create virtual table for Full-Text Search + +Revision ID: 129ac51807e4 +Revises: 8974efc51d22 +Create Date: 2021-08-13 19:13:54.112538 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '129ac51807e4' +down_revision = '8974efc51d22' +branch_labels = None +depends_on = None + + +def upgrade(): + queries = [ + "CREATE VIRTUAL TABLE pubsub_items_fts " + "USING fts5(data, content=pubsub_items, content_rowid=id)", + "CREATE TRIGGER pubsub_items_fts_sync_ins AFTER INSERT ON pubsub_items BEGIN" + " INSERT INTO pubsub_items_fts(rowid, data) VALUES (new.id, new.data);" + "END", + "CREATE TRIGGER pubsub_items_fts_sync_del AFTER DELETE ON pubsub_items BEGIN" + " INSERT INTO pubsub_items_fts(pubsub_items_fts, rowid, data) " + "VALUES('delete', old.id, old.data);" + "END", + "CREATE TRIGGER pubsub_items_fts_sync_upd AFTER UPDATE ON pubsub_items BEGIN" + " INSERT INTO pubsub_items_fts(pubsub_items_fts, rowid, data) VALUES" + "('delete', old.id, old.data);" + " INSERT INTO pubsub_items_fts(rowid, data) VALUES(new.id, new.data);" + "END", + "INSERT INTO pubsub_items_fts(rowid, data) SELECT id, data from pubsub_items" + ] + for q in queries: + op.execute(sa.DDL(q)) + + +def downgrade(): + queries = [ + "DROP TRIGGER IF EXISTS pubsub_items_fts_sync_ins", + "DROP TRIGGER IF EXISTS pubsub_items_fts_sync_del", + "DROP TRIGGER IF EXISTS pubsub_items_fts_sync_upd", + "DROP TABLE IF EXISTS pubsub_items_fts", + ] + for q in queries: + op.execute(sa.DDL(q)) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/4b002773cf92_add_origin_id_column_to_history_and_.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/versions/4b002773cf92_add_origin_id_column_to_history_and_.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,60 @@ +"""add origin_id column to history and adapt constraints + +Revision ID: 4b002773cf92 +Revises: 79e5f3313fa4 +Create Date: 2022-06-13 16:10:39.711634 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4b002773cf92' +down_revision = '79e5f3313fa4' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('history', schema=None) as batch_op: + batch_op.add_column(sa.Column('origin_id', sa.Text(), nullable=True)) + batch_op.create_unique_constraint('uq_origin_id', ['profile_id', 'origin_id', 'source']) + + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.alter_column('history_uid', + existing_type=sa.TEXT(), + nullable=False) + batch_op.alter_column('message', + existing_type=sa.TEXT(), + nullable=False) + + with op.batch_alter_table('subject', schema=None) as batch_op: + batch_op.alter_column('history_uid', + existing_type=sa.TEXT(), + nullable=False) + batch_op.alter_column('subject', + existing_type=sa.TEXT(), + nullable=False) + + +def downgrade(): + with op.batch_alter_table('subject', schema=None) as batch_op: + batch_op.alter_column('subject', + existing_type=sa.TEXT(), + nullable=True) + batch_op.alter_column('history_uid', + existing_type=sa.TEXT(), + nullable=True) + + with op.batch_alter_table('message', schema=None) as batch_op: + batch_op.alter_column('message', + existing_type=sa.TEXT(), + nullable=True) + batch_op.alter_column('history_uid', + existing_type=sa.TEXT(), + nullable=True) + + with op.batch_alter_table('history', schema=None) as batch_op: + batch_op.drop_constraint('uq_origin_id', type_='unique') + batch_op.drop_column('origin_id') diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/602caf848068_drop_message_types_table_fix_nullable.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/versions/602caf848068_drop_message_types_table_fix_nullable.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,410 @@ +"""drop message_types table + fix nullable + rename constraints + +Revision ID: 602caf848068 +Revises: +Create Date: 2021-06-26 12:42:54.148313 + +""" +from alembic import op +from sqlalchemy import ( + Table, + Column, + MetaData, + TEXT, + INTEGER, + Text, + Integer, + Float, + Enum, + ForeignKey, + Index, + PrimaryKeyConstraint, +) +from sqlalchemy.sql import table, column + + +# revision identifiers, used by Alembic. +revision = "602caf848068" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # we have to recreate former tables for batch_alter_table's reflexion, otherwise the + # database will be used, and this will keep unammed UNIQUE constraints in addition + # to the named ones that we create + metadata = MetaData( + naming_convention={ + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", + }, + ) + + old_profiles_table = Table( + "profiles", + metadata, + Column("id", Integer, primary_key=True, nullable=True, autoincrement=False), + Column("name", Text, unique=True), + ) + + old_components_table = Table( + "components", + metadata, + Column( + "profile_id", + ForeignKey("profiles.id", ondelete="CASCADE"), + nullable=True, + primary_key=True, + ), + Column("entry_point", Text, nullable=False), + ) + + old_message_table = Table( + "message", + metadata, + Column("id", Integer, primary_key=True, nullable=True, autoincrement=False), + Column("history_uid", ForeignKey("history.uid", ondelete="CASCADE")), + Column("message", Text), + Column("language", Text), + Index("message__history_uid", "history_uid"), + ) + + old_subject_table = Table( + "subject", + metadata, + Column("id", Integer, primary_key=True, nullable=True, autoincrement=False), + Column("history_uid", ForeignKey("history.uid", ondelete="CASCADE")), + Column("subject", Text), + Column("language", Text), + Index("subject__history_uid", "history_uid"), + ) + + old_thread_table = Table( + "thread", + metadata, + Column("id", Integer, primary_key=True, nullable=True, autoincrement=False), + Column("history_uid", ForeignKey("history.uid", ondelete="CASCADE")), + Column("thread_id", Text), + Column("parent_id", Text), + Index("thread__history_uid", "history_uid"), + ) + + old_history_table = Table( + "history", + metadata, + Column("uid", Text, primary_key=True, nullable=True), + Column("stanza_id", Text), + Column("update_uid", Text), + Column("profile_id", Integer, ForeignKey("profiles.id", ondelete="CASCADE")), + Column("source", Text), + Column("dest", Text), + Column("source_res", Text), + Column("dest_res", Text), + Column("timestamp", Float, nullable=False), + Column("received_timestamp", Float), + Column("type", Text, ForeignKey("message_types.type")), + Column("extra", Text), + Index("history__profile_id_timestamp", "profile_id", "timestamp"), + Index( + "history__profile_id_received_timestamp", "profile_id", "received_timestamp" + ), + ) + + old_param_gen_table = Table( + "param_gen", + metadata, + Column("category", Text, primary_key=True), + Column("name", Text, primary_key=True), + Column("value", Text), + ) + + old_param_ind_table = Table( + "param_ind", + metadata, + Column("category", Text, primary_key=True), + Column("name", Text, primary_key=True), + Column( + "profile_id", ForeignKey("profiles.id", ondelete="CASCADE"), primary_key=True + ), + Column("value", Text), + ) + + old_private_gen_table = Table( + "private_gen", + metadata, + Column("namespace", Text, primary_key=True), + Column("key", Text, primary_key=True), + Column("value", Text), + ) + + old_private_ind_table = Table( + "private_ind", + metadata, + Column("namespace", Text, primary_key=True), + Column("key", Text, primary_key=True), + Column( + "profile_id", ForeignKey("profiles.id", ondelete="CASCADE"), primary_key=True + ), + Column("value", Text), + ) + + old_private_gen_bin_table = Table( + "private_gen_bin", + metadata, + Column("namespace", Text, primary_key=True), + Column("key", Text, primary_key=True), + Column("value", Text), + ) + + old_private_ind_bin_table = Table( + "private_ind_bin", + metadata, + Column("namespace", Text, primary_key=True), + Column("key", Text, primary_key=True), + Column( + "profile_id", ForeignKey("profiles.id", ondelete="CASCADE"), primary_key=True + ), + Column("value", Text), + ) + + old_files_table = Table( + "files", + metadata, + Column("id", Text, primary_key=True), + Column("public_id", Text, unique=True), + Column("version", Text, primary_key=True), + Column("parent", Text, nullable=False), + Column( + "type", + Enum("file", "directory", name="file_type", create_constraint=True), + nullable=False, + server_default="file", + ), + Column("file_hash", Text), + Column("hash_algo", Text), + Column("name", Text, nullable=False), + Column("size", Integer), + Column("namespace", Text), + Column("media_type", Text), + Column("media_subtype", Text), + Column("created", Float, nullable=False), + Column("modified", Float), + Column("owner", Text), + Column("access", Text), + Column("extra", Text), + Column("profile_id", ForeignKey("profiles.id", ondelete="CASCADE")), + Index("files__profile_id_owner_parent", "profile_id", "owner", "parent"), + Index( + "files__profile_id_owner_media_type_media_subtype", + "profile_id", + "owner", + "media_type", + "media_subtype", + ), + ) + + op.drop_table("message_types") + + with op.batch_alter_table( + "profiles", copy_from=old_profiles_table, schema=None + ) as batch_op: + batch_op.create_unique_constraint(batch_op.f("uq_profiles_name"), ["name"]) + + with op.batch_alter_table( + "components", + copy_from=old_components_table, + naming_convention={ + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + }, + schema=None, + ) as batch_op: + batch_op.create_unique_constraint(batch_op.f("uq_profiles_name"), ["name"]) + + with op.batch_alter_table( + "history", + copy_from=old_history_table, + naming_convention={ + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + }, + schema=None, + ) as batch_op: + batch_op.alter_column("uid", existing_type=TEXT(), nullable=False) + batch_op.alter_column( + "type", + type_=Enum( + "chat", + "error", + "groupchat", + "headline", + "normal", + "info", + name="message_type", + create_constraint=True, + ), + existing_type=TEXT(), + nullable=False, + ) + batch_op.create_unique_constraint( + batch_op.f("uq_history_profile_id"), + ["profile_id", "stanza_id", "source", "dest"], + ) + batch_op.drop_constraint("fk_history_type_message_types", type_="foreignkey") + + with op.batch_alter_table( + "message", copy_from=old_message_table, schema=None + ) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=False, autoincrement=False + ) + + with op.batch_alter_table( + "subject", copy_from=old_subject_table, schema=None + ) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=False, autoincrement=False + ) + + with op.batch_alter_table( + "thread", copy_from=old_thread_table, schema=None + ) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=False, autoincrement=False + ) + + with op.batch_alter_table( + "param_gen", copy_from=old_param_gen_table, schema=None + ) as batch_op: + batch_op.alter_column("category", existing_type=TEXT(), nullable=False) + batch_op.alter_column("name", existing_type=TEXT(), nullable=False) + + with op.batch_alter_table( + "param_ind", copy_from=old_param_ind_table, schema=None + ) as batch_op: + batch_op.alter_column("category", existing_type=TEXT(), nullable=False) + batch_op.alter_column("name", existing_type=TEXT(), nullable=False) + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=False) + + with op.batch_alter_table( + "private_gen", copy_from=old_private_gen_table, schema=None + ) as batch_op: + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=False) + batch_op.alter_column("key", existing_type=TEXT(), nullable=False) + + with op.batch_alter_table( + "private_ind", copy_from=old_private_ind_table, schema=None + ) as batch_op: + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=False) + batch_op.alter_column("key", existing_type=TEXT(), nullable=False) + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=False) + + with op.batch_alter_table( + "private_gen_bin", copy_from=old_private_gen_bin_table, schema=None + ) as batch_op: + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=False) + batch_op.alter_column("key", existing_type=TEXT(), nullable=False) + + # found some invalid rows in local database, maybe old values made during development, + # but in doubt we have to delete them + op.execute("DELETE FROM private_ind_bin WHERE namespace IS NULL") + + with op.batch_alter_table( + "private_ind_bin", copy_from=old_private_ind_bin_table, schema=None + ) as batch_op: + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=False) + batch_op.alter_column("key", existing_type=TEXT(), nullable=False) + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=False) + + with op.batch_alter_table( + "files", copy_from=old_files_table, schema=None + ) as batch_op: + batch_op.create_unique_constraint(batch_op.f("uq_files_public_id"), ["public_id"]) + batch_op.alter_column( + "type", + type_=Enum("file", "directory", name="file_type", create_constraint=True), + existing_type=Text(), + nullable=False, + ) + + +def downgrade(): + # downgrade doesn't restore the exact same state as before upgrade, as it + # would be useless and waste of resource to restore broken things such as + # anonymous constraints + with op.batch_alter_table("thread", schema=None) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=True, autoincrement=False + ) + + with op.batch_alter_table("subject", schema=None) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=True, autoincrement=False + ) + + with op.batch_alter_table("private_ind_bin", schema=None) as batch_op: + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=True) + batch_op.alter_column("key", existing_type=TEXT(), nullable=True) + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("private_ind", schema=None) as batch_op: + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=True) + batch_op.alter_column("key", existing_type=TEXT(), nullable=True) + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("private_gen_bin", schema=None) as batch_op: + batch_op.alter_column("key", existing_type=TEXT(), nullable=True) + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("private_gen", schema=None) as batch_op: + batch_op.alter_column("key", existing_type=TEXT(), nullable=True) + batch_op.alter_column("namespace", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("param_ind", schema=None) as batch_op: + batch_op.alter_column("profile_id", existing_type=INTEGER(), nullable=True) + batch_op.alter_column("name", existing_type=TEXT(), nullable=True) + batch_op.alter_column("category", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("param_gen", schema=None) as batch_op: + batch_op.alter_column("name", existing_type=TEXT(), nullable=True) + batch_op.alter_column("category", existing_type=TEXT(), nullable=True) + + with op.batch_alter_table("message", schema=None) as batch_op: + batch_op.alter_column( + "id", existing_type=INTEGER(), nullable=True, autoincrement=False + ) + + op.create_table( + "message_types", + Column("type", TEXT(), nullable=True), + PrimaryKeyConstraint("type"), + ) + message_types_table = table("message_types", column("type", TEXT())) + op.bulk_insert( + message_types_table, + [ + {"type": "chat"}, + {"type": "error"}, + {"type": "groupchat"}, + {"type": "headline"}, + {"type": "normal"}, + {"type": "info"}, + ], + ) + + with op.batch_alter_table("history", schema=None) as batch_op: + batch_op.alter_column( + "type", + type_=TEXT(), + existing_type=TEXT(), + nullable=True, + ) + batch_op.create_foreign_key( + batch_op.f("fk_history_type_message_types"), + "message_types", + ["type"], + ["type"], + ) + batch_op.alter_column("uid", existing_type=TEXT(), nullable=True) diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/79e5f3313fa4_create_table_for_pubsub_subscriptions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/versions/79e5f3313fa4_create_table_for_pubsub_subscriptions.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,33 @@ +"""create table for pubsub subscriptions + +Revision ID: 79e5f3313fa4 +Revises: 129ac51807e4 +Create Date: 2022-03-14 17:15:00.689871 + +""" +from alembic import op +import sqlalchemy as sa +from libervia.backend.memory.sqla_mapping import JID + + +# revision identifiers, used by Alembic. +revision = '79e5f3313fa4' +down_revision = '129ac51807e4' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table('pubsub_subs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('node_id', sa.Integer(), nullable=False), + sa.Column('subscriber', JID(), nullable=True), + sa.Column('state', sa.Enum('SUBSCRIBED', 'PENDING', name='state'), nullable=True), + sa.ForeignKeyConstraint(['node_id'], ['pubsub_nodes.id'], name=op.f('fk_pubsub_subs_node_id_pubsub_nodes'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pubsub_subs')), + sa.UniqueConstraint('node_id', 'subscriber', name=op.f('uq_pubsub_subs_node_id')) + ) + + +def downgrade(): + op.drop_table('pubsub_subs') diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/8974efc51d22_create_tables_for_pubsub_caching.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/migration/versions/8974efc51d22_create_tables_for_pubsub_caching.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,57 @@ +"""create tables for Pubsub caching + +Revision ID: 8974efc51d22 +Revises: 602caf848068 +Create Date: 2021-07-27 16:38:54.658212 + +""" +from alembic import op +import sqlalchemy as sa +from libervia.backend.memory.sqla_mapping import JID, Xml + + +# revision identifiers, used by Alembic. +revision = '8974efc51d22' +down_revision = '602caf848068' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('pubsub_nodes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('profile_id', sa.Integer(), nullable=True), + sa.Column('service', JID(), nullable=True), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('subscribed', sa.Boolean(create_constraint=True, name='subscribed_bool'), nullable=False), + sa.Column('analyser', sa.Text(), nullable=True), + sa.Column('sync_state', sa.Enum('IN_PROGRESS', 'COMPLETED', 'ERROR', 'NO_SYNC', name='sync_state', create_constraint=True), nullable=True), + sa.Column('sync_state_updated', sa.Float(), nullable=False), + sa.Column('type', sa.Text(), nullable=True), + sa.Column('subtype', sa.Text(), nullable=True), + sa.Column('extra', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], name=op.f('fk_pubsub_nodes_profile_id_profiles'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pubsub_nodes')), + sa.UniqueConstraint('profile_id', 'service', 'name', name=op.f('uq_pubsub_nodes_profile_id')) + ) + op.create_table('pubsub_items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('node_id', sa.Integer(), nullable=False), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('data', Xml(), nullable=False), + sa.Column('created', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('parsed', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['node_id'], ['pubsub_nodes.id'], name=op.f('fk_pubsub_items_node_id_pubsub_nodes'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_pubsub_items')), + sa.UniqueConstraint('node_id', 'name', name=op.f('uq_pubsub_items_node_id')) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pubsub_items') + op.drop_table('pubsub_nodes') + # ### end Alembic commands ### diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/migration/versions/__init__.py diff -r d10748475025 -r 4b842c1fb686 libervia/backend/memory/params.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/memory/params.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,1173 @@ +#!/usr/bin/env python3 + +# Libervia: an XMPP client +# Copyright (C) 2009-2021 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 Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libervia.backend.core.i18n import _, D_ + +from libervia.backend.core import exceptions +from libervia.backend.core.constants import Const as C +from libervia.backend.memory.crypto import BlockCipher, PasswordHasher +from xml.dom import minidom, NotFoundErr +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from twisted.internet import defer +from twisted.python.failure import Failure +from twisted.words.xish import domish +from twisted.words.protocols.jabber import jid +from libervia.backend.tools.xml_tools import params_xml_2_xmlui, get_text +from libervia.backend.tools.common import data_format +from xml.sax.saxutils import quoteattr + +# TODO: params should be rewritten using Twisted directly instead of minidom +# general params should be linked to sat.conf and kept synchronised +# this need an overall simplification to make maintenance easier + + +def create_jid_elts(jids): + """Generator which return elements from jids + + @param jids(iterable[id.jID]): jids to use + @return (generator[domish.Element]): elements + """ + for jid_ in jids: + jid_elt = domish.Element((None, "jid")) + jid_elt.addContent(jid_.full()) + yield jid_elt + + +class Params(object): + """This class manage parameters with xml""" + + ### TODO: add desciption in params + + # TODO: when priority is changed, a new presence stanza must be emitted + # TODO: int type (Priority should be int instead of string) + default_xml = """ + + + + + + + + + + + + + + + + + + + + + + + + """ % { + "category_general": D_("General"), + "category_connection": D_("Connection"), + "history_param": C.HISTORY_LIMIT, + "history_label": D_("Chat history limit"), + "show_offline_contacts": C.SHOW_OFFLINE_CONTACTS, + "show_offline_contacts_label": D_("Show offline contacts"), + "show_empty_groups": C.SHOW_EMPTY_GROUPS, + "show_empty_groups_label": D_("Show empty groups"), + "force_server_param": C.FORCE_SERVER_PARAM, + "force_port_param": C.FORCE_PORT_PARAM, + "autoconnect_backend_label": D_("Connect on backend startup"), + "autoconnect_label": D_("Connect on frontend startup"), + "autodisconnect_label": D_("Disconnect on frontend closure"), + "check_certificate_label": D_("Check certificate (don't uncheck if unsure)"), + } + + def load_default_params(self): + self.dom = minidom.parseString(Params.default_xml.encode("utf-8")) + + def _merge_params(self, source_node, dest_node): + """Look for every node in source_node and recursively copy them to dest if they don't exists""" + + def get_nodes_map(children): + ret = {} + for child in children: + if child.nodeType == child.ELEMENT_NODE: + ret[(child.tagName, child.getAttribute("name"))] = child + return ret + + source_map = get_nodes_map(source_node.childNodes) + dest_map = get_nodes_map(dest_node.childNodes) + source_set = set(source_map.keys()) + dest_set = set(dest_map.keys()) + to_add = source_set.difference(dest_set) + + for node_key in to_add: + dest_node.appendChild(source_map[node_key].cloneNode(True)) + + to_recurse = source_set - to_add + for node_key in to_recurse: + self._merge_params(source_map[node_key], dest_map[node_key]) + + def load_xml(self, xml_file): + """Load parameters template from xml file""" + self.dom = minidom.parse(xml_file) + default_dom = minidom.parseString(Params.default_xml.encode("utf-8")) + self._merge_params(default_dom.documentElement, self.dom.documentElement) + + def load_gen_params(self): + """Load general parameters data from storage + + @return: deferred triggered once params are loaded + """ + return self.storage.load_gen_params(self.params_gen) + + def load_ind_params(self, profile, cache=None): + """Load individual parameters + + set self.params cache or a temporary cache + @param profile: profile to load (*must exist*) + @param cache: if not None, will be used to store the value, as a short time cache + @return: deferred triggered once params are loaded + """ + if cache is None: + self.params[profile] = {} + return self.storage.load_ind_params( + self.params[profile] if cache is None else cache, profile + ) + + def purge_profile(self, profile): + """Remove cache data of a profile + + @param profile: %(doc_profile)s + """ + try: + del self.params[profile] + except KeyError: + log.error( + _("Trying to purge cache of a profile not in memory: [%s]") % profile + ) + + def save_xml(self, filename): + """Save parameters template to xml file""" + with open(filename, "wb") as xml_file: + xml_file.write(self.dom.toxml("utf-8")) + + def __init__(self, host, storage): + log.debug("Parameters init") + self.host = host + self.storage = storage + self.default_profile = None + self.params = {} + self.params_gen = {} + + def create_profile(self, profile, component): + """Create a new profile + + @param profile(unicode): name of the profile + @param component(unicode): entry point if profile is a component + @param callback: called when the profile actually exists in database and memory + @return: a Deferred instance + """ + if self.storage.has_profile(profile): + log.info(_("The profile name already exists")) + return defer.fail(exceptions.ConflictError()) + if not self.host.trigger.point("ProfileCreation", profile): + return defer.fail(exceptions.CancelError()) + return self.storage.create_profile(profile, component or None) + + def profile_delete_async(self, profile, force=False): + """Delete an existing profile + + @param profile: name of the profile + @param force: force the deletion even if the profile is connected. + To be used for direct calls only (not through the bridge). + @return: a Deferred instance + """ + if not self.storage.has_profile(profile): + log.info(_("Trying to delete an unknown profile")) + return defer.fail(Failure(exceptions.ProfileUnknownError(profile))) + if self.host.is_connected(profile): + if force: + self.host.disconnect(profile) + else: + log.info(_("Trying to delete a connected profile")) + return defer.fail(Failure(exceptions.ProfileConnected)) + return self.storage.delete_profile(profile) + + def get_profile_name(self, profile_key, return_profile_keys=False): + """return profile according to profile_key + + @param profile_key: profile name or key which can be + C.PROF_KEY_ALL for all profiles + C.PROF_KEY_DEFAULT for default profile + @param return_profile_keys: if True, return unmanaged profile keys (like + C.PROF_KEY_ALL). This keys must be managed by the caller + @return: requested profile name + @raise exceptions.ProfileUnknownError: profile doesn't exists + @raise exceptions.ProfileNotSetError: if C.PROF_KEY_NONE is used + """ + if profile_key == "@DEFAULT@": + default = self.host.memory.memory_data.get("Profile_default") + if not default: + log.info(_("No default profile, returning first one")) + try: + default = self.host.memory.memory_data[ + "Profile_default" + ] = self.storage.get_profiles_list()[0] + except IndexError: + log.info(_("No profile exist yet")) + raise exceptions.ProfileUnknownError(profile_key) + return ( + default + ) # FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists + elif profile_key == C.PROF_KEY_NONE: + raise exceptions.ProfileNotSetError + elif return_profile_keys and profile_key in [C.PROF_KEY_ALL]: + return profile_key # this value must be managed by the caller + if not self.storage.has_profile(profile_key): + log.error(_("Trying to access an unknown profile (%s)") % profile_key) + raise exceptions.ProfileUnknownError(profile_key) + 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 update_params(self, xml, security_limit=C.NO_SECURITY_LIMIT, app=""): + """import xml in parameters, update if the param already exists + + If security_limit is specified and greater than -1, the parameters + that have a security level greater than security_limit are skipped. + @param xml: parameters in xml form + @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure + @param app: name of the frontend registering the parameters or empty value + """ + # TODO: should word with domish.Element + src_parent = minidom.parseString(xml.encode("utf-8")).documentElement + + def pre_process_app_node(src_parent, security_limit, app): + """Parameters that are registered from a frontend must be checked""" + to_remove = [] + for type_node in src_parent.childNodes: + if type_node.nodeName != C.INDIVIDUAL: + to_remove.append(type_node) # accept individual parameters only + continue + for cat_node in type_node.childNodes: + if cat_node.nodeName != "category": + to_remove.append(cat_node) + continue + to_remove_count = ( + 0 + ) # count the params to be removed from current category + for node in cat_node.childNodes: + if node.nodeName != "param" or not self.check_security_limit( + node, security_limit + ): + to_remove.append(node) + to_remove_count += 1 + continue + node.setAttribute("app", app) + if ( + len(cat_node.childNodes) == to_remove_count + ): # remove empty category + for __ in range(0, to_remove_count): + to_remove.pop() + to_remove.append(cat_node) + for node in to_remove: + node.parentNode.removeChild(node) + + 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.cloneNode(True)) + else: + if child.nodeName == "param": + # The child updates an existing parameter, we replace the node + tgt_parent.replaceChild(child, node) + else: + # the node already exists, we recurse 1 more level + import_node(node, child) + + if app: + pre_process_app_node(src_parent, security_limit, app) + import_node(self.dom.documentElement, src_parent) + + def params_register_app(self, xml, security_limit, app): + """Register frontend's specific parameters + + If security_limit is specified and greater than -1, the parameters + that have a security level greater than security_limit are skipped. + @param xml: XML definition of the parameters to be added + @param security_limit: -1 means no security, 0 is the maximum security then the higher the less secure + @param app: name of the frontend registering the parameters + """ + if not app: + log.warning( + _( + "Trying to register frontends parameters with no specified app: aborted" + ) + ) + return + if not hasattr(self, "frontends_cache"): + self.frontends_cache = [] + if app in self.frontends_cache: + log.debug( + _( + "Trying to register twice frontends parameters for %(app)s: aborted" + % {"app": app} + ) + ) + return + self.frontends_cache.append(app) + self.update_params(xml, security_limit, app) + log.debug("Frontends parameters registered for %(app)s" % {"app": app}) + + def __default_ok(self, value, name, category): + # FIXME: will not work with individual parameters + self.param_set(name, value, category) + + def __default_ko(self, failure, name, category): + log.error( + _("Can't determine default value for [%(category)s/%(name)s]: %(reason)s") + % {"category": category, "name": name, "reason": str(failure.value)} + ) + + def set_default(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 + # TODO: manage individual paramaters + log.debug( + "set_default called for %(category)s/%(name)s" + % {"category": category, "name": name} + ) + node = self._get_param_node(name, category, "@ALL@") + if not node: + log.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'] # default_cb is not used anymore as a flag to know if we have to set the default value, + # and we can still use it later e.g. to call a generic set_default method + value = self._get_param(category, name, C.GENERAL) + if value is None: # no value set by the user: we have the default value + log.debug("Default value to set, using callback") + d = defer.maybeDeferred(callback) + d.addCallback(self.__default_ok, name, category) + d.addErrback(errback or self.__default_ko, name, category) + + def _get_attr_internal(self, node, attr, value): + """Get attribute value. + + /!\ This method would return encrypted password values. + + @param node: XML param node + @param attr: name of the attribute to get (e.g.: 'value' or 'type') + @param value: user defined value + @return: value (can be str, bool, int, list, None) + """ + if attr == "value": + value_to_use = ( + value if value is not None else node.getAttribute(attr) + ) # we use value (user defined) if it exist, else we use node's default value + if node.getAttribute("type") == "bool": + return C.bool(value_to_use) + if node.getAttribute("type") == "int": + return int(value_to_use) if value_to_use else value_to_use + elif node.getAttribute("type") == "list": + if ( + not value_to_use + ): # no user defined value, take default value from the XML + options = [ + option + for option in node.childNodes + if option.nodeName == "option" + ] + selected = [ + option + for option in options + if option.getAttribute("selected") == "true" + ] + cat, param = ( + node.parentNode.getAttribute("name"), + node.getAttribute("name"), + ) + if len(selected) == 1: + value_to_use = selected[0].getAttribute("value") + log.info( + _( + "Unset parameter (%(cat)s, %(param)s) of type list will use the default option '%(value)s'" + ) + % {"cat": cat, "param": param, "value": value_to_use} + ) + return value_to_use + if len(selected) == 0: + log.error( + _( + "Parameter (%(cat)s, %(param)s) of type list has no default option!" + ) + % {"cat": cat, "param": param} + ) + else: + log.error( + _( + "Parameter (%(cat)s, %(param)s) of type list has more than one default option!" + ) + % {"cat": cat, "param": param} + ) + raise exceptions.DataError + elif node.getAttribute("type") == "jids_list": + if value_to_use: + jids = value_to_use.split( + "\t" + ) # FIXME: it's not good to use tabs as separator ! + else: # no user defined value, take default value from the XML + jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")] + to_delete = [] + for idx, value in enumerate(jids): + try: + jids[idx] = jid.JID(value) + except (RuntimeError, jid.InvalidFormat, AttributeError): + log.warning( + "Incorrect jid value found in jids list: [{}]".format(value) + ) + to_delete.append(value) + for value in to_delete: + jids.remove(value) + return jids + return value_to_use + return node.getAttribute(attr) + + def _get_attr(self, node, attr, value): + """Get attribute value (synchronous). + + /!\ This method can not be used to retrieve password values. + @param node: XML param node + @param attr: name of the attribute to get (e.g.: 'value' or 'type') + @param value: user defined value + @return (unicode, bool, int, list): value to retrieve + """ + if attr == "value" and node.getAttribute("type") == "password": + raise exceptions.InternalError( + "To retrieve password values, use _async_get_attr instead of _get_attr" + ) + return self._get_attr_internal(node, attr, value) + + def _async_get_attr(self, node, attr, value, profile=None): + """Get attribute value. + + Profile passwords are returned hashed (if not empty), + other passwords are returned decrypted (if not empty). + @param node: XML param node + @param attr: name of the attribute to get (e.g.: 'value' or 'type') + @param value: user defined value + @param profile: %(doc_profile)s + @return (unicode, bool, int, list): Deferred value to retrieve + """ + value = self._get_attr_internal(node, attr, value) + if attr != "value" or node.getAttribute("type") != "password": + return defer.succeed(value) + param_cat = node.parentNode.getAttribute("name") + param_name = node.getAttribute("name") + if ((param_cat, param_name) == C.PROFILE_PASS_PATH) or not value: + return defer.succeed( + value + ) # profile password and empty passwords are returned "as is" + if not profile: + raise exceptions.ProfileNotSetError( + "The profile is needed to decrypt a password" + ) + password = self.host.memory.decrypt_value(value, profile) + + if password is None: + raise exceptions.InternalError("password should never be None") + return defer.succeed(password) + + def _type_to_str(self, result): + """Convert result to string, according to its type """ + if isinstance(result, bool): + return C.bool_const(result) + elif isinstance(result, (list, set, tuple)): + return ', '.join(self._type_to_str(r) for r in result) + else: + return str(result) + + def get_string_param_a(self, name, category, attr="value", profile_key=C.PROF_KEY_NONE): + """ Same as param_get_a but for bridge: convert non string value to string """ + return self._type_to_str( + self.param_get_a(name, category, attr, profile_key=profile_key) + ) + + def param_get_a( + self, name, category, attr="value", use_default=True, profile_key=C.PROF_KEY_NONE + ): + """Helper method to get a specific attribute. + + /!\ This method would return encrypted password values, + to get the plain values you have to use param_get_a_async. + @param name: name of the parameter + @param category: category of the parameter + @param attr: name of the attribute (default: "value") + @parm use_default(bool): if True and attr=='value', return default value if not set + else return None if not set + @param profile: owner of the param (@ALL@ for everyone) + @return: attribute + """ + # FIXME: looks really dirty and buggy, need to be reviewed/refactored + # FIXME: security_limit is not managed here ! + node = self._get_param_node(name, category) + if not node: + log.error( + _( + "Requested param [%(name)s] in category [%(category)s] doesn't exist !" + ) + % {"name": name, "category": category} + ) + raise exceptions.NotFound + + if attr == "value" and node[1].getAttribute("type") == "password": + raise exceptions.InternalError( + "To retrieve password values, use param_get_a_async instead of param_get_a" + ) + + if node[0] == C.GENERAL: + value = self._get_param(category, name, C.GENERAL) + if value is None and attr == "value" and not use_default: + return value + return self._get_attr(node[1], attr, value) + + assert node[0] == C.INDIVIDUAL + + profile = self.get_profile_name(profile_key) + if not profile: + log.error(_("Requesting a param for an non-existant profile")) + raise exceptions.ProfileUnknownError(profile_key) + + if profile not in self.params: + log.error(_("Requesting synchronous param for not connected profile")) + raise exceptions.ProfileNotConnected(profile) + + if attr == "value": + value = self._get_param(category, name, profile=profile) + if value is None and attr == "value" and not use_default: + return value + return self._get_attr(node[1], attr, value) + + async def async_get_string_param_a( + self, name, category, attr="value", security_limit=C.NO_SECURITY_LIMIT, + profile=C.PROF_KEY_NONE): + value = await self.param_get_a_async( + name, category, attr, security_limit, profile_key=profile) + return self._type_to_str(value) + + def param_get_a_async( + self, + name, + category, + attr="value", + security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE, + ): + """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 (defer.Deferred): parameter value, with corresponding type (bool, int, list, etc) + """ + node = self._get_param_node(name, category) + if not node: + log.error( + _( + "Requested param [%(name)s] in category [%(category)s] doesn't exist !" + ) + % {"name": name, "category": category} + ) + raise ValueError("Requested param doesn't exist") + + if not self.check_security_limit(node[1], security_limit): + log.warning( + _( + "Trying to get parameter '%(param)s' in category '%(cat)s' without authorization!!!" + % {"param": name, "cat": category} + ) + ) + raise exceptions.PermissionError + + if node[0] == C.GENERAL: + value = self._get_param(category, name, C.GENERAL) + return self._async_get_attr(node[1], attr, value) + + assert node[0] == C.INDIVIDUAL + + profile = self.get_profile_name(profile_key) + if not profile: + raise exceptions.InternalError( + _("Requesting a param for a non-existant profile") + ) + + if attr != "value": + return defer.succeed(node[1].getAttribute(attr)) + try: + value = self._get_param(category, name, profile=profile) + return self._async_get_attr(node[1], attr, value, profile) + except exceptions.ProfileNotInCacheError: + # We have to ask data to the storage manager + d = self.storage.get_ind_param(category, name, profile) + return d.addCallback( + lambda value: self._async_get_attr(node[1], attr, value, profile) + ) + + def _get_params_values_from_category( + self, category, security_limit, app, extra_s, profile_key): + client = self.host.get_client(profile_key) + extra = data_format.deserialise(extra_s) + return defer.ensureDeferred(self.get_params_values_from_category( + client, category, security_limit, app, extra)) + + async def get_params_values_from_category( + self, client, category, security_limit, app='', extra=None): + """Get all parameters "attribute" for a category + + @param category(unicode): the desired category + @param security_limit(int): NO_SECURITY_LIMIT (-1) to return all the params. + Otherwise sole the params which have a security level defined *and* + lower or equal to the specified value are returned. + @param app(str): see [get_params] + @param extra(dict): see [get_params] + @return (dict): key: param name, value: param value (converted to string if needed) + """ + # TODO: manage category of general type (without existant profile) + if extra is None: + extra = {} + prof_xml = await self._construct_profile_xml(client, security_limit, app, extra) + ret = {} + for category_node in prof_xml.getElementsByTagName("category"): + if category_node.getAttribute("name") == category: + for param_node in category_node.getElementsByTagName("param"): + name = param_node.getAttribute("name") + if not name: + log.warning( + "ignoring attribute without name: {}".format( + param_node.toxml() + ) + ) + continue + value = await self.async_get_string_param_a( + name, category, security_limit=security_limit, + profile=client.profile) + + ret[name] = value + break + + prof_xml.unlink() + return ret + + def _get_param( + self, category, name, type_=C.INDIVIDUAL, cache=None, profile=C.PROF_KEY_NONE + ): + """Return the param, or None if it doesn't exist + + @param category: param category + @param name: param name + @param type_: GENERAL or INDIVIDUAL + @param cache: temporary cache, to use when profile is not logged + @param profile: the profile name (not profile key, i.e. name and not something like @DEFAULT@) + @return: param value or None if it doesn't exist + """ + if type_ == C.GENERAL: + if (category, name) in self.params_gen: + return self.params_gen[(category, name)] + return None # This general param has the default value + assert type_ == C.INDIVIDUAL + if profile == C.PROF_KEY_NONE: + raise exceptions.ProfileNotSetError + if profile in self.params: + cache = self.params[profile] # if profile is in main cache, we use it, + # ignoring the temporary cache + elif ( + cache is None + ): # else we use the temporary cache if it exists, or raise an exception + raise exceptions.ProfileNotInCacheError + if (category, name) not in cache: + return None + return cache[(category, name)] + + async def _construct_profile_xml(self, client, security_limit, app, extra): + """Construct xml for asked profile, filling values when needed + + /!\ as noticed in doc, don't forget to unlink the minidom.Document + @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. + Otherwise sole the params which have a security level defined *and* + lower or equal to the specified value are returned. + @param app: name of the frontend requesting the parameters, or '' to get all parameters + @param profile: profile name (not key !) + @return: a deferred that fire a minidom.Document of the profile xml (cf warning above) + """ + profile = client.profile + + def check_node(node): + """Check the node against security_limit, app and extra""" + return (self.check_security_limit(node, security_limit) + and self.check_app(node, app) + and self.check_extra(node, extra)) + + if profile in self.params: + profile_cache = self.params[profile] + else: + # profile is not in cache, we load values in a short time cache + profile_cache = {} + await self.load_ind_params(profile, profile_cache) + + # init the result document + prof_xml = minidom.parseString("") + cache = {} + + for type_node in self.dom.documentElement.childNodes: + if type_node.nodeName != C.GENERAL and type_node.nodeName != C.INDIVIDUAL: + continue + # we use all params, general and individual + for cat_node in type_node.childNodes: + if cat_node.nodeName != "category": + continue + category = cat_node.getAttribute("name") + dest_params = {} # result (merged) params for category + if category not in cache: + # we make a copy for the new xml + cache[category] = dest_cat = cat_node.cloneNode(True) + to_remove = [] + for node in dest_cat.childNodes: + if node.nodeName != "param": + continue + if not check_node(node): + to_remove.append(node) + continue + dest_params[node.getAttribute("name")] = node + for node in to_remove: + dest_cat.removeChild(node) + new_node = True + else: + # It's not a new node, we use the previously cloned one + dest_cat = cache[category] + new_node = False + params = cat_node.getElementsByTagName("param") + + for param_node in params: + # we have to merge new params (we are parsing individual parameters, we have to add them + # to the previously parsed general ones) + name = param_node.getAttribute("name") + if not check_node(param_node): + continue + if name not in dest_params: + # this is reached when a previous category exists + dest_params[name] = param_node.cloneNode(True) + dest_cat.appendChild(dest_params[name]) + + profile_value = self._get_param( + category, + name, + type_node.nodeName, + cache=profile_cache, + profile=profile, + ) + if profile_value is not None: + # there is a value for this profile, we must change the default + if dest_params[name].getAttribute("type") == "list": + for option in dest_params[name].getElementsByTagName( + "option" + ): + if option.getAttribute("value") == profile_value: + option.setAttribute("selected", "true") + else: + try: + option.removeAttribute("selected") + except NotFoundErr: + pass + elif dest_params[name].getAttribute("type") == "jids_list": + jids = profile_value.split("\t") + for jid_elt in dest_params[name].getElementsByTagName( + "jid" + ): + dest_params[name].removeChild( + jid_elt + ) # remove all default + for jid_ in jids: # rebuilt the children with use values + try: + jid.JID(jid_) + except ( + RuntimeError, + jid.InvalidFormat, + AttributeError, + ): + log.warning( + "Incorrect jid value found in jids list: [{}]".format( + jid_ + ) + ) + else: + jid_elt = prof_xml.createElement("jid") + jid_elt.appendChild(prof_xml.createTextNode(jid_)) + dest_params[name].appendChild(jid_elt) + else: + dest_params[name].setAttribute("value", profile_value) + if new_node: + prof_xml.documentElement.appendChild(dest_cat) + + to_remove = [] + for cat_node in prof_xml.documentElement.childNodes: + # we remove empty categories + if cat_node.getElementsByTagName("param").length == 0: + to_remove.append(cat_node) + for node in to_remove: + prof_xml.documentElement.removeChild(node) + + return prof_xml + + + def _get_params_ui(self, security_limit, app, extra_s, profile_key): + client = self.host.get_client(profile_key) + extra = data_format.deserialise(extra_s) + return defer.ensureDeferred(self.param_ui_get(client, security_limit, app, extra)) + + async def param_ui_get(self, client, security_limit, app, extra=None): + """Get XMLUI to handle parameters + + @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. + Otherwise sole the params which have a security level defined *and* + lower or equal to the specified value are returned. + @param app: name of the frontend requesting the parameters, or '' to get all parameters + @param extra (dict, None): extra options. Key can be: + - ignore: list of (category/name) values to remove from parameters + @return(str): a SàT XMLUI for parameters + """ + param_xml = await self.get_params(client, security_limit, app, extra) + return params_xml_2_xmlui(param_xml) + + async def get_params(self, client, security_limit, app, extra=None): + """Construct xml for asked profile, take params xml as skeleton + + @param security_limit: NO_SECURITY_LIMIT (-1) to return all the params. + Otherwise sole the params which have a security level defined *and* + lower or equal to the specified value are returned. + @param app: name of the frontend requesting the parameters, or '' to get all parameters + @param extra (dict, None): extra options. Key can be: + - ignore: list of (category/name) values to remove from parameters + @param profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile. + @return: XML of parameters + """ + if extra is None: + extra = {} + prof_xml = await self._construct_profile_xml(client, security_limit, app, extra) + return_xml = prof_xml.toxml() + prof_xml.unlink() + return "\n".join((line for line in return_xml.split("\n") if line)) + + def _get_param_node(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 + @param type_: keyword for search: + @ALL@ search everywhere + @GENERAL@ only search in general type + @INDIVIDUAL@ only search in individual type + @return: a tuple (node type, node) or None if not found""" + + for type_node in self.dom.documentElement.childNodes: + if ( + (type_ == "@ALL@" or type_ == "@GENERAL@") + and type_node.nodeName == C.GENERAL + ) or ( + (type_ == "@ALL@" or type_ == "@INDIVIDUAL@") + and type_node.nodeName == C.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 params_categories_get(self): + """return the categories availables""" + categories = [] + for cat in self.dom.getElementsByTagName("category"): + name = cat.getAttribute("name") + if name not in categories: + categories.append(cat.getAttribute("name")) + return categories + + def param_set(self, name, value, category, security_limit=C.NO_SECURITY_LIMIT, + profile_key=C.PROF_KEY_NONE): + """Set a parameter, return None if the parameter is not in param xml. + + Parameter of type 'password' that are not the SàT profile password are + stored encrypted (if not empty). The profile password is stored hashed + (if not empty). + + @param name (str): the parameter name + @param value (str): the new value + @param category (str): the parameter category + @param security_limit (int) + @param profile_key (str): %(doc_profile_key)s + @return: a deferred None value when everything is done + """ + # FIXME: param_set should accept the right type for value, not only str ! + if profile_key != C.PROF_KEY_NONE: + profile = self.get_profile_name(profile_key) + if not profile: + log.error(_("Trying to set parameter for an unknown profile")) + raise exceptions.ProfileUnknownError(profile_key) + + node = self._get_param_node(name, category, "@ALL@") + if not node: + log.error( + _("Requesting an unknown parameter (%(category)s/%(name)s)") + % {"category": category, "name": name} + ) + return defer.succeed(None) + + if not self.check_security_limit(node[1], security_limit): + msg = _( + "{profile!r} is trying to set parameter {name!r} in category " + "{category!r} without authorization!!!").format( + profile=repr(profile), + name=repr(name), + category=repr(category) + ) + log.warning(msg) + raise exceptions.PermissionError(msg) + + type_ = node[1].getAttribute("type") + if type_ == "int": + if not value: # replace with the default value (which might also be '') + value = node[1].getAttribute("value") + else: + try: + int(value) + except ValueError: + log.warning(_( + "Trying to set parameter {name} in category {category} with" + "an non-integer value" + ).format( + name=repr(name), + category=repr(category) + )) + return defer.succeed(None) + if node[1].hasAttribute("constraint"): + constraint = node[1].getAttribute("constraint") + try: + min_, max_ = [int(limit) for limit in constraint.split(";")] + except ValueError: + raise exceptions.InternalError( + "Invalid integer parameter constraint: %s" % constraint + ) + value = str(min(max(int(value), min_), max_)) + + log.info( + _("Setting parameter (%(category)s, %(name)s) = %(value)s") + % { + "category": category, + "name": name, + "value": value if type_ != "password" else "********", + } + ) + + if node[0] == C.GENERAL: + self.params_gen[(category, name)] = value + self.storage.set_gen_param(category, name, value) + for profile in self.storage.get_profiles_list(): + if self.host.memory.is_session_started(profile): + self.host.bridge.param_update(name, value, category, profile) + self.host.trigger.point( + "param_update_trigger", name, value, category, node[0], profile + ) + return defer.succeed(None) + + assert node[0] == C.INDIVIDUAL + assert profile_key != C.PROF_KEY_NONE + + if type_ == "button": + log.debug("Clicked param button %s" % node.toxml()) + return defer.succeed(None) + elif type_ == "password": + try: + personal_key = self.host.memory.auth_sessions.profile_get_unique(profile)[ + C.MEMORY_CRYPTO_KEY + ] + except TypeError: + raise exceptions.InternalError( + _("Trying to encrypt a password while the personal key is undefined!") + ) + if (category, name) == C.PROFILE_PASS_PATH: + # using 'value' as the encryption key to encrypt another encryption key... could be confusing! + d = self.host.memory.encrypt_personal_data( + data_key=C.MEMORY_CRYPTO_KEY, + data_value=personal_key, + crypto_key=value, + profile=profile, + ) + d.addCallback( + lambda __: PasswordHasher.hash(value) + ) # profile password is hashed (empty value stays empty) + elif value: # other non empty passwords are encrypted with the personal key + d = defer.succeed(BlockCipher.encrypt(personal_key, value)) + else: + d = defer.succeed(value) + else: + d = defer.succeed(value) + + def got_final_value(value): + if self.host.memory.is_session_started(profile): + self.params[profile][(category, name)] = value + self.host.bridge.param_update(name, value, category, profile) + self.host.trigger.point( + "param_update_trigger", name, value, category, node[0], profile + ) + return self.storage.set_ind_param(category, name, value, profile) + else: + raise exceptions.ProfileNotConnected + + d.addCallback(got_final_value) + return d + + def _get_nodes_of_types(self, attr_type, node_type="@ALL@"): + """Return all the nodes matching the given types. + + TODO: using during the dev but not anymore... remove if not needed + + @param attr_type (str): the attribute type (string, text, password, bool, int, button, list) + @param node_type (str): keyword for filtering: + @ALL@ search everywhere + @GENERAL@ only search in general type + @INDIVIDUAL@ only search in individual type + @return: dict{tuple: node}: a dict {key, value} where: + - key is a couple (attribute category, attribute name) + - value is a node + """ + ret = {} + for type_node in self.dom.documentElement.childNodes: + if ( + (node_type == "@ALL@" or node_type == "@GENERAL@") + and type_node.nodeName == C.GENERAL + ) or ( + (node_type == "@ALL@" or node_type == "@INDIVIDUAL@") + and type_node.nodeName == C.INDIVIDUAL + ): + for cat_node in type_node.getElementsByTagName("category"): + cat = cat_node.getAttribute("name") + params = cat_node.getElementsByTagName("param") + for param in params: + if param.getAttribute("type") == attr_type: + ret[(cat, param.getAttribute("name"))] = param + return ret + + def check_security_limit(self, node, security_limit): + """Check the given node against the given security limit. + The value NO_SECURITY_LIMIT (-1) means that everything is allowed. + @return: True if this node can be accessed with the given security limit. + """ + if security_limit < 0: + return True + if node.hasAttribute("security"): + if int(node.getAttribute("security")) <= security_limit: + return True + return False + + def check_app(self, node, app): + """Check the given node against the given app. + + @param node: parameter node + @param app: name of the frontend requesting the parameters, or '' to get all parameters + @return: True if this node concerns the given app. + """ + if not app or not node.hasAttribute("app"): + return True + return node.getAttribute("app") == app + + def check_extra(self, node, extra): + """Check the given node against the extra filters. + + @param node: parameter node + @param app: name of the frontend requesting the parameters, or '' to get all parameters + @return: True if node doesn't match category/name of extra['ignore'] list + """ + ignore_list = extra.get('ignore') + if not ignore_list: + return True + category = node.parentNode.getAttribute('name') + name = node.getAttribute('name') + ignore = [category, name] in ignore_list + if ignore: + log.debug(f"Ignoring parameter {category}/{name} as requested") + return False + return True + + +def make_options(options, selected=None): + """Create option XML form dictionary + + @param options(dict): option's name => option's label map + @param selected(None, str): value of selected option + None to use first value + @return (str): XML to use in parameters + """ + str_list = [] + if selected is None: + selected = next(iter(options.keys())) + selected_found = False + for value, label in options.items(): + if value == selected: + selected = 'selected="true"' + selected_found = True + else: + selected = '' + str_list.append( + f'