sat-0.6.1.1+hg20180208/0002755000175500017600000000000013243470056014001 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/i18n/0002755000175500017600000000000013243470024014553 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/i18n/sat.po0000644000175500017600000034102513243470024015705 0ustar debaclelocal_src# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-03-06 21:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: frontends/src/bridge/dbus_bridge.py:78 #: src/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py:227 #: src/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:78 #: src/bridge/bridge_constructor/generated/dbus_bridge.py:78 #: src/bridge/dbus_bridge.py:578 msgid "" "D-Bus is not launched, please see README to see instructions on how to " "launch it" msgstr "" #: frontends/src/bridge/dbus_bridge.py:91 #: src/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py:91 #: src/bridge/bridge_constructor/generated/dbus_bridge.py:91 msgid "Unknown interface" msgstr "" #: frontends/src/jp/base.py:115 msgid "" "ProgressBar not available, please download it at http://pypi.python.org/pypi/" "progressbar" msgstr "" #: frontends/src/jp/base.py:116 msgid "" "Progress bar deactivated\n" "--\n" msgstr "" #: frontends/src/jp/base.py:173 msgid "Available commands" msgstr "" #: frontends/src/jp/base.py:188 frontends/src/quick_frontend/quick_app.py:299 msgid "Can't connect to SàT backend, are you sure it's launched ?" msgstr "" #: frontends/src/jp/base.py:190 frontends/src/quick_frontend/quick_app.py:302 msgid "Can't init bridge" msgstr "" #: frontends/src/jp/base.py:192 frontends/src/quick_frontend/quick_app.py:305 msgid "Error while initialising bridge: {}" msgstr "" #: frontends/src/jp/base.py:291 #, python-format msgid "Use PROFILE profile key (default: %(default)s)" msgstr "" #: frontends/src/jp/base.py:292 msgid "Password used to connect profile, if necessary" msgstr "" #: frontends/src/jp/base.py:296 msgid "Connect the profile before doing anything else" msgstr "" #: frontends/src/jp/base.py:301 msgid "Start a profile session without connecting" msgstr "" #: frontends/src/jp/base.py:305 msgid "Show progress bar" msgstr "" #: frontends/src/jp/base.py:308 msgid "Add a verbosity level (can be used multiple times)" msgstr "" #: frontends/src/jp/base.py:336 msgid "Can't import {} plugin, ignoring it" msgstr "" #: frontends/src/jp/base.py:340 #, python-brace-format msgid "Missing module for plugin {name}: {missing}" msgstr "" #: frontends/src/jp/base.py:354 #, python-brace-format msgid "Invalid plugin module [{type}] {module}" msgstr "" #: frontends/src/jp/base.py:368 msgid "User interruption: good bye" msgstr "" #: frontends/src/jp/base.py:437 #, python-format msgid "%s is not a valid JID !" msgstr "" #: frontends/src/jp/base.py:461 #, python-brace-format msgid "Can't connect profile: {reason}" msgstr "" #: frontends/src/jp/base.py:465 #, python-brace-format msgid "Can't start {profile}'s session: {reason}" msgstr "" #: frontends/src/jp/base.py:471 #, python-brace-format msgid "The profile [{profile}] doesn't exist" msgstr "" #: frontends/src/jp/base.py:485 #, python-brace-format msgid "" "Session for [{profile}] is not started, please start it before using jp, or " "use either --start-session or --connect option" msgstr "" #: frontends/src/jp/base.py:501 #, python-brace-format msgid "" "Profile [{profile}] is not connected, please connect it before using jp, or " "use --connect option" msgstr "" #: frontends/src/jp/base.py:582 msgid "select output format (default: {})" msgstr "" #: frontends/src/jp/base.py:583 msgid "output specific option" msgstr "" #: frontends/src/jp/base.py:667 msgid "file size is not known, we can't show a progress bar" msgstr "" #: frontends/src/jp/base.py:672 msgid "Progress: " msgstr "" #: frontends/src/jp/base.py:696 msgid "Operation started" msgstr "" #: frontends/src/jp/base.py:712 msgid "Operation successfully finished" msgstr "" #: frontends/src/jp/base.py:719 msgid "Error while doing operation: {}" msgstr "" #: frontends/src/jp/cmd_adhoc.py:30 msgid "Remote control a software" msgstr "" #: frontends/src/jp/cmd_adhoc.py:33 msgid "Software name" msgstr "" #: frontends/src/jp/cmd_adhoc.py:34 msgid "Jids allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:35 msgid "Groups allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:36 msgid "Groups that are *NOT* allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:37 msgid "Jids that are *NOT* allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:38 msgid "Loop on the commands" msgstr "" #: frontends/src/jp/cmd_adhoc.py:52 msgid "No bus name found" msgstr "" #: frontends/src/jp/cmd_adhoc.py:54 #, python-format msgid "Bus name found: [%s]" msgstr "" #: frontends/src/jp/cmd_adhoc.py:57 #, python-format msgid "Command found: (path:%(path)s, iface: %(iface)s) [%(command)s]" msgstr "" #: frontends/src/jp/cmd_adhoc.py:66 msgid "Ad-hoc commands" msgstr "" #: frontends/src/jp/cmd_avatar.py:36 msgid "set avatar of the profile" msgstr "" #: frontends/src/jp/cmd_avatar.py:40 msgid "path to the image to upload" msgstr "" #: frontends/src/jp/cmd_avatar.py:46 frontends/src/jp/cmd_file.py:82 #: frontends/src/jp/cmd_file.py:264 msgid "file [{}] doesn't exist !" msgstr "" #: frontends/src/jp/cmd_avatar.py:52 msgid "avatar has been set" msgstr "" #: frontends/src/jp/cmd_avatar.py:56 #, python-brace-format msgid "error while uploading avatar: {msg}" msgstr "" #: frontends/src/jp/cmd_avatar.py:63 msgid "retrieve avatar of an entity" msgstr "" #: frontends/src/jp/cmd_avatar.py:67 msgid "entity" msgstr "" #: frontends/src/jp/cmd_avatar.py:68 msgid "show avatar" msgstr "" #: frontends/src/jp/cmd_avatar.py:90 msgid "No avatar found." msgstr "" #: frontends/src/jp/cmd_avatar.py:100 #, python-brace-format msgid "error while getting avatar: {msg}" msgstr "" #: frontends/src/jp/cmd_avatar.py:111 msgid "avatar uploading/retrieving" msgstr "" #: frontends/src/jp/cmd_blog.py:197 msgid "get blog item(s)" msgstr "" #: frontends/src/jp/cmd_blog.py:201 msgid "PubSub node to request" msgstr "" #: frontends/src/jp/cmd_blog.py:203 msgid "item(s) id(s) to get (default: request all items)" msgstr "" #: frontends/src/jp/cmd_blog.py:204 msgid "maximum number of items to get ({} to get all items)" msgstr "" #: frontends/src/jp/cmd_blog.py:207 msgid "microblog data key(s) to display (default: depend of verbosity)" msgstr "" #: frontends/src/jp/cmd_blog.py:209 msgid "JID of the PubSub service (default: request profile own blog)" msgstr "" #: frontends/src/jp/cmd_blog.py:369 msgid "edit an existing or new blog post" msgstr "" #: frontends/src/jp/cmd_blog.py:373 msgid "URL of the item to edit, or keyword" msgstr "" #: frontends/src/jp/cmd_blog.py:374 msgid "launch a blog preview in parallel" msgstr "" #: frontends/src/jp/cmd_blog.py:375 msgid "title of the item" msgstr "" #: frontends/src/jp/cmd_blog.py:376 msgid "tag (category) of your item" msgstr "" #: frontends/src/jp/cmd_blog.py:377 msgid "disable comments" msgstr "" #: frontends/src/jp/cmd_blog.py:633 msgid "preview a blog content" msgstr "" #: frontends/src/jp/cmd_blog.py:637 msgid "use inotify to handle preview" msgstr "" #: frontends/src/jp/cmd_blog.py:638 msgid "path to the content file" msgstr "" #: frontends/src/jp/cmd_blog.py:775 msgid "import an external blog" msgstr "" #: frontends/src/jp/cmd_blog.py:779 msgid "importer name, nothing to display importers list" msgstr "" #: frontends/src/jp/cmd_blog.py:780 msgid "original blog host" msgstr "" #: frontends/src/jp/cmd_blog.py:781 msgid "do *NOT* upload images (default: do upload images)" msgstr "" #: frontends/src/jp/cmd_blog.py:782 msgid "do not upload images from this host (default: upload all images)" msgstr "" #: frontends/src/jp/cmd_blog.py:783 msgid "ignore invalide TLS certificate for uploads" msgstr "" #: frontends/src/jp/cmd_blog.py:785 msgid "importer specific options (see importer description)" msgstr "" #: frontends/src/jp/cmd_blog.py:787 msgid "PubSub service where the items must be uploaded (default: server)" msgstr "" #: frontends/src/jp/cmd_blog.py:789 msgid "" "importer data location (see importer description), nothing to show importer " "description" msgstr "" #: frontends/src/jp/cmd_blog.py:792 msgid "Blog upload started" msgstr "" #: frontends/src/jp/cmd_blog.py:795 msgid "Blog uploaded successfully" msgstr "" #: frontends/src/jp/cmd_blog.py:805 #, python-brace-format 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 "" #: frontends/src/jp/cmd_blog.py:808 msgid "Error while uploading blog: {}" msgstr "" #: frontends/src/jp/cmd_blog.py:811 #, python-brace-format msgid "Error while trying to upload a blog: {reason}" msgstr "" #: frontends/src/jp/cmd_blog.py:818 #, python-brace-format msgid "{name} argument can't be used without location argument" msgstr "" #: frontends/src/jp/cmd_blog.py:854 msgid "blog/microblog management" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:32 #, python-format msgid "storage location (default: %(default)s)" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:33 #, python-format msgid "bookmarks type (default: %(default)s)" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:42 msgid "list bookmarks" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:70 msgid "remove a bookmark" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:75 frontends/src/jp/cmd_bookmarks.py:89 msgid "jid (for muc bookmark) or url of to remove" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:84 src/plugins/plugin_xep_0048.py:252 msgid "add a bookmark" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:90 msgid "bookmark name" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:91 msgid "MUC specific options" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:92 msgid "nickname" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:93 msgid "join room on profile connection" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:98 msgid "You can't use --autojoin or --nick with --type url" msgstr "" #: frontends/src/jp/cmd_bookmarks.py:114 msgid "manage bookmarks" msgstr "" #: frontends/src/jp/cmd_debug.py:49 msgid "call a bridge method" msgstr "" #: frontends/src/jp/cmd_debug.py:54 msgid "name of the method to execute" msgstr "" #: frontends/src/jp/cmd_debug.py:55 msgid "argument of the method" msgstr "" #: frontends/src/jp/cmd_debug.py:63 msgid "Error while executing {}: {}" msgstr "" #: frontends/src/jp/cmd_debug.py:76 msgid "bad arguments" msgstr "" #: frontends/src/jp/cmd_debug.py:82 msgid "send a fake signal from backend" msgstr "" #: frontends/src/jp/cmd_debug.py:86 msgid "name of the signal to send" msgstr "" #: frontends/src/jp/cmd_debug.py:87 msgid "argument of the signal" msgstr "" #: frontends/src/jp/cmd_debug.py:101 msgid "bridge s(t)imulation" msgstr "" #: frontends/src/jp/cmd_debug.py:108 msgid "debugging tools" msgstr "" #: frontends/src/jp/cmd_file.py:38 msgid "Send a file to a contact" msgstr "" #: frontends/src/jp/cmd_file.py:42 msgid "a list of file" msgstr "" #: frontends/src/jp/cmd_file.py:43 frontends/src/jp/cmd_message.py:44 #: frontends/src/jp/cmd_pipe.py:42 msgid "the destination jid" msgstr "" #: frontends/src/jp/cmd_file.py:44 msgid "make a bzip2 tarball" msgstr "" #: frontends/src/jp/cmd_file.py:51 frontends/src/jp/cmd_file.py:121 msgid "File copy started" msgstr "" #: frontends/src/jp/cmd_file.py:54 msgid "File sent successfully" msgstr "" #: frontends/src/jp/cmd_file.py:57 msgid "Error while sending file: {}" msgstr "" #: frontends/src/jp/cmd_file.py:66 #, python-brace-format msgid "File request sent to {jid}" msgstr "" #: frontends/src/jp/cmd_file.py:71 #, python-brace-format msgid "Can't send file to {jid}" msgstr "" #: frontends/src/jp/cmd_file.py:75 #, python-brace-format msgid "Error while trying to send a file: {reason}" msgstr "" #: frontends/src/jp/cmd_file.py:85 msgid "[{}] is a dir ! Please send files inside or use compression" msgstr "" #: frontends/src/jp/cmd_file.py:93 msgid "bz2 is an experimental option, use with caution" msgstr "" #: frontends/src/jp/cmd_file.py:95 msgid "Starting compression, please wait..." msgstr "" #: frontends/src/jp/cmd_file.py:100 msgid "Adding {}" msgstr "" #: frontends/src/jp/cmd_file.py:103 msgid "Done !" msgstr "" #: frontends/src/jp/cmd_file.py:115 msgid "Wait for a file to be sent by a contact" msgstr "" #: frontends/src/jp/cmd_file.py:124 msgid "File received successfully" msgstr "" #: frontends/src/jp/cmd_file.py:127 #, python-brace-format msgid "hash checked: {algo}:{checksum}" msgstr "" #: frontends/src/jp/cmd_file.py:132 msgid "hash is checked but hash value is missing" msgstr "" #: frontends/src/jp/cmd_file.py:134 msgid "hash can't be verified" msgstr "" #: frontends/src/jp/cmd_file.py:137 msgid "Error while receiving file: {}" msgstr "" #: frontends/src/jp/cmd_file.py:146 frontends/src/jp/cmd_pipe.py:73 msgid "Action has no XMLUI" msgstr "" #: frontends/src/jp/cmd_file.py:151 frontends/src/jp/cmd_pipe.py:78 msgid "Invalid XMLUI received" msgstr "" #: frontends/src/jp/cmd_file.py:161 frontends/src/jp/cmd_pipe.py:88 msgid "Ignoring action without from_jid data" msgstr "" #: frontends/src/jp/cmd_file.py:166 frontends/src/jp/cmd_file.py:185 msgid "ignoring action without progress id" msgstr "" #: frontends/src/jp/cmd_file.py:171 msgid "File refused because overwrite is needed" msgstr "" #: frontends/src/jp/cmd_file.py:187 msgid "Overwriting needed" msgstr "" #: frontends/src/jp/cmd_file.py:191 msgid "Overwrite accepted" msgstr "" #: frontends/src/jp/cmd_file.py:193 msgid "Refused to overwrite" msgstr "" #: frontends/src/jp/cmd_file.py:200 msgid "JIDs accepted (accept everything if none is specified)" msgstr "" #: frontends/src/jp/cmd_file.py:201 msgid "accept multiple files (you'll have to stop manually)" msgstr "" #: frontends/src/jp/cmd_file.py:202 msgid "force overwritting of existing files (/!\\ name is choosed by sended)" msgstr "" #: frontends/src/jp/cmd_file.py:203 msgid "destination path (default: working directory)" msgstr "" #: frontends/src/jp/cmd_file.py:209 msgid "Given path is not a directory !" msgstr "" #: frontends/src/jp/cmd_file.py:213 msgid "waiting for incoming file request" msgstr "" #: frontends/src/jp/cmd_file.py:219 msgid "Upload a file" msgstr "" #: frontends/src/jp/cmd_file.py:223 msgid "file to upload" msgstr "" #: frontends/src/jp/cmd_file.py:224 msgid "jid of upload component (nothing to autodetect)" msgstr "" #: frontends/src/jp/cmd_file.py:225 msgid "ignore invalide TLS certificate" msgstr "" #: frontends/src/jp/cmd_file.py:228 msgid "File upload started" msgstr "" #: frontends/src/jp/cmd_file.py:231 msgid "File uploaded successfully" msgstr "" #: frontends/src/jp/cmd_file.py:237 msgid "URL to retrieve the file:" msgstr "" #: frontends/src/jp/cmd_file.py:242 msgid "Error while uploading file: {}" msgstr "" #: frontends/src/jp/cmd_file.py:254 msgid "Can't upload file" msgstr "" #: frontends/src/jp/cmd_file.py:258 #, python-brace-format msgid "Error while trying to upload a file: {reason}" msgstr "" #: frontends/src/jp/cmd_file.py:267 msgid "[{}] is a dir! Can't upload a dir" msgstr "" #: frontends/src/jp/cmd_file.py:283 msgid "File sending/receiving" msgstr "" #: frontends/src/jp/cmd_info.py:30 msgid "service discovery" msgstr "" #: frontends/src/jp/cmd_info.py:34 msgid "entity to discover" msgstr "" #: frontends/src/jp/cmd_info.py:35 msgid "node to use" msgstr "" #: frontends/src/jp/cmd_info.py:43 #, python-format msgid "Error while doing discovery [%s]" msgstr "" #: frontends/src/jp/cmd_info.py:90 #, python-brace-format msgid "" "Features:\n" "\n" "{features}" msgstr "" #: frontends/src/jp/cmd_info.py:92 #, python-brace-format msgid "" "Identities:\n" "\n" "{identities}" msgstr "" #: frontends/src/jp/cmd_info.py:94 #, python-brace-format msgid "" "Extensions:\n" "\n" "{extensions}" msgstr "" #: frontends/src/jp/cmd_info.py:96 #, python-brace-format msgid "" "Items:\n" "\n" "{items}" msgstr "" #: frontends/src/jp/cmd_info.py:109 msgid "running session" msgstr "" #: frontends/src/jp/cmd_info.py:113 msgid "Entity to request" msgstr "" #: frontends/src/jp/cmd_info.py:121 #, python-format msgid "Error while trying to get version [%s]" msgstr "" #: frontends/src/jp/cmd_info.py:128 src/plugins/plugin_xep_0092.py:107 #, python-format msgid "Client name: %s" msgstr "" #: frontends/src/jp/cmd_info.py:130 src/plugins/plugin_xep_0092.py:109 #, python-format msgid "Client version: %s" msgstr "" #: frontends/src/jp/cmd_info.py:132 #, python-format msgid "Operating System: %s" msgstr "" #: frontends/src/jp/cmd_info.py:141 msgid "client version" msgstr "" #: frontends/src/jp/cmd_info.py:155 msgid "Error getting session infos: {}" msgstr "" #: frontends/src/jp/cmd_info.py:163 msgid "Get various pieces of information on entities" msgstr "" #: frontends/src/jp/cmd_message.py:32 msgid "send a message to a contact" msgstr "" #: frontends/src/jp/cmd_message.py:35 msgid "language of the message" msgstr "" #: frontends/src/jp/cmd_message.py:36 msgid "" "separate xmpp messages: send one message per line instead of one message " "alone." msgstr "" #: frontends/src/jp/cmd_message.py:37 msgid "add a new line at the beginning of the input (usefull for ascii art ;))" msgstr "" #: frontends/src/jp/cmd_message.py:38 msgid "subject of the message" msgstr "" #: frontends/src/jp/cmd_message.py:39 msgid "language of subject" msgstr "" #: frontends/src/jp/cmd_message.py:40 msgid "type of the message" msgstr "" #: frontends/src/jp/cmd_message.py:42 msgid "XHTML body" msgstr "" #: frontends/src/jp/cmd_message.py:43 msgid "rich body" msgstr "" #: frontends/src/jp/cmd_message.py:91 msgid "messages handling" msgstr "" #: frontends/src/jp/cmd_param.py:29 msgid "Get a parameter value" msgstr "" #: frontends/src/jp/cmd_param.py:32 frontends/src/jp/cmd_param.py:64 msgid "Category of the parameter" msgstr "" #: frontends/src/jp/cmd_param.py:33 frontends/src/jp/cmd_param.py:65 #: frontends/src/jp/cmd_param.py:66 msgid "Name of the parameter" msgstr "" #: frontends/src/jp/cmd_param.py:34 msgid "Name of the attribute to get" msgstr "" #: frontends/src/jp/cmd_param.py:35 frontends/src/jp/cmd_param.py:67 msgid "Security limit" msgstr "" #: frontends/src/jp/cmd_param.py:61 msgid "Set a parameter value" msgstr "" #: frontends/src/jp/cmd_param.py:78 msgid "Save parameters template to xml file" msgstr "" #: frontends/src/jp/cmd_param.py:81 msgid "Output file" msgstr "" #: frontends/src/jp/cmd_param.py:86 #, python-format msgid "Parameters saved to file %s" msgstr "" #: frontends/src/jp/cmd_param.py:88 #, python-format msgid "Can't save parameters to file %s" msgstr "" #: frontends/src/jp/cmd_param.py:94 msgid "Load parameters template from xml file" msgstr "" #: frontends/src/jp/cmd_param.py:97 msgid "Input file" msgstr "" #: frontends/src/jp/cmd_param.py:102 #, python-format msgid "Parameters loaded from file %s" msgstr "" #: frontends/src/jp/cmd_param.py:104 #, python-format msgid "Can't load parameters from file %s" msgstr "" #: frontends/src/jp/cmd_param.py:111 msgid "Save/load parameters template" msgstr "" #: frontends/src/jp/cmd_pipe.py:38 msgid "send a pipe a stream" msgstr "" #: frontends/src/jp/cmd_pipe.py:60 msgid "receive a pipe stream" msgstr "" #: frontends/src/jp/cmd_pipe.py:64 msgid "Jids accepted (none means \"accept everything\")" msgstr "" #: frontends/src/jp/cmd_pipe.py:111 msgid "stream piping through XMPP" msgstr "" #: frontends/src/jp/cmd_profile.py:30 msgid "The name of the profile" msgstr "" #: frontends/src/jp/cmd_profile.py:62 msgid "delete profile without confirmation" msgstr "" #: frontends/src/jp/cmd_profile.py:72 msgid "Profile deletion cancelled" msgstr "" #: frontends/src/jp/cmd_profile.py:80 msgid "get information about a profile" msgstr "" #: frontends/src/jp/cmd_profile.py:82 src/plugins/plugin_xep_0048.py:256 msgid "jid" msgstr "" #: frontends/src/jp/cmd_profile.py:87 msgid "show the XMPP password IN CLEAR TEXT" msgstr "" #: frontends/src/jp/cmd_profile.py:103 msgid "XMPP password" msgstr "" #: frontends/src/jp/cmd_profile.py:113 msgid "the password of the profile" msgstr "" #: frontends/src/jp/cmd_profile.py:139 msgid "set to component import name (entry point) if this is a component" msgstr "" #: frontends/src/jp/cmd_profile.py:171 msgid "set as default profile" msgstr "" #: frontends/src/jp/cmd_roster.py:35 msgid "Purge the roster from its contacts with no subscription" msgstr "" #: frontends/src/jp/cmd_roster.py:39 msgid "Also purge contacts with no 'from' subscription" msgstr "" #: frontends/src/jp/cmd_roster.py:40 msgid "Also purge contacts with no 'to' subscription" msgstr "" #: frontends/src/jp/cmd_roster.py:46 frontends/src/jp/cmd_roster.py:110 #: frontends/src/jp/cmd_roster.py:181 #, python-format msgid "Error while retrieving the contacts [%s]" msgstr "" #: frontends/src/jp/cmd_roster.py:100 msgid "Show statistics about a roster" msgstr "" #: frontends/src/jp/cmd_roster.py:169 msgid "Retrieve the roster contacts" msgstr "" #: frontends/src/jp/cmd_roster.py:173 msgid "Show the contacts' subscriptions" msgstr "" #: frontends/src/jp/cmd_roster.py:174 msgid "Show the contacts' groups" msgstr "" #: frontends/src/jp/cmd_roster.py:175 msgid "Show the contacts' names" msgstr "" #: frontends/src/jp/cmd_roster.py:214 msgid "Manage an entity's roster" msgstr "" #: frontends/src/jp/output_template.py:100 msgid "" "Browser opening requested.\n" "Temporary files are put in the following directory, you'll have to delete it " "yourself once finished viewing: {}" msgstr "" #: frontends/src/primitivus/chat.py:37 msgid "{} occupants" msgstr "" #: frontends/src/primitivus/chat.py:346 msgid "Game" msgstr "" #: frontends/src/primitivus/chat.py:429 #, python-brace-format msgid "<= {nick} has left the room ({count})" msgstr "" #: frontends/src/primitivus/chat.py:431 #, python-brace-format msgid "<=> {nick} re-entered the room ({count})" msgstr "" #: frontends/src/primitivus/chat.py:466 #, python-brace-format msgid "You have been mentioned by {nick} in {room}" msgstr "" #: frontends/src/primitivus/chat.py:473 #, python-brace-format msgid "{entity} is talking to you" msgstr "" #: frontends/src/primitivus/chat.py:560 msgid "Results for searching the globbing pattern: {}" msgstr "" #: frontends/src/primitivus/chat.py:561 msgid "Type ':history ' to reset the chat history" msgstr "" #: frontends/src/primitivus/chat.py:591 #, python-format msgid "Primitivus: %s is talking to you" msgstr "" #: frontends/src/primitivus/chat.py:593 #, python-format msgid "Primitivus: %(user)s mentioned you in room '%(room)s'" msgstr "" #: frontends/src/primitivus/chat.py:599 msgid "Can't start game" msgstr "" #: frontends/src/primitivus/chat.py:599 msgid "You need to be exactly 4 peoples in the room to start a Tarot game" msgstr "" #: frontends/src/primitivus/chat.py:623 msgid "Change title" msgstr "" #: frontends/src/primitivus/chat.py:624 msgid "Enter the new title" msgstr "" #: frontends/src/primitivus/contact_list.py:47 #: frontends/src/primitivus/primitivus:533 src/plugins/plugin_xep_0055.py:80 #: src/stdui/ui_contact_list.py:36 src/stdui/ui_contact_list.py:37 #: src/stdui/ui_contact_list.py:38 msgid "Contacts" msgstr "" #: frontends/src/primitivus/game_tarot.py:267 msgid "Please choose your contrat" msgstr "" #: frontends/src/primitivus/game_tarot.py:283 #: src/plugins/plugin_misc_tarot.py:244 msgid "Draw game" msgstr "" #: frontends/src/primitivus/game_tarot.py:285 msgid "You win \\o/" msgstr "" #: frontends/src/primitivus/game_tarot.py:285 msgid "You loose :(" msgstr "" #: frontends/src/primitivus/game_tarot.py:297 msgid "Cards played are invalid !" msgstr "" #: frontends/src/primitivus/game_tarot.py:330 msgid "Do you put these cards in chien ?" msgstr "" #: frontends/src/primitivus/primitivus:91 msgid "Error while sending message ({})" msgstr "" #: frontends/src/primitivus/primitivus:136 msgid "Please specify the globbing pattern to search for" msgstr "" #: frontends/src/primitivus/primitivus:371 msgid "Configuration Error" msgstr "" #: frontends/src/primitivus/primitivus:371 msgid "" "Something went wrong while reading the configuration, please check :messages" msgstr "" #: frontends/src/primitivus/primitivus:498 msgid "Pleeeeasse, I can't even breathe !" msgstr "" #: frontends/src/primitivus/primitivus:527 src/memory/params.py:81 msgid "General" msgstr "" #: frontends/src/primitivus/primitivus:528 #: frontends/src/primitivus/profile_manager.py:53 msgid "Connect" msgstr "" #: frontends/src/primitivus/primitivus:529 src/plugins/plugin_xep_0050.py:59 msgid "Disconnect" msgstr "" #: frontends/src/primitivus/primitivus:530 msgid "Parameters" msgstr "" #: frontends/src/primitivus/primitivus:531 #: frontends/src/primitivus/primitivus:821 msgid "About" msgstr "" #: frontends/src/primitivus/primitivus:532 msgid "Exit" msgstr "" #: frontends/src/primitivus/primitivus:534 src/plugins/plugin_xep_0048.py:61 msgid "Groups" msgstr "" #: frontends/src/primitivus/primitivus:536 msgid "Join room" msgstr "" #: frontends/src/primitivus/primitivus:541 msgid "Main menu" msgstr "" #: frontends/src/primitivus/primitivus:636 #, python-brace-format msgid "{app}: a new event has just happened{entity}" msgstr "" #: frontends/src/primitivus/primitivus:706 msgid "Chat menu" msgstr "" #: frontends/src/primitivus/primitivus:760 #: frontends/src/primitivus/primitivus:801 #: frontends/src/quick_frontend/quick_profile_manager.py:130 #: src/plugins/plugin_misc_account.py:495 #: src/plugins/plugin_misc_account.py:538 #: src/plugins/plugin_misc_account.py:595 msgid "Error" msgstr "" #: frontends/src/primitivus/primitivus:760 msgid "Unmanaged action" msgstr "" #: frontends/src/primitivus/primitivus:771 msgid "unkown" msgstr "" #: frontends/src/primitivus/primitivus:801 #, python-format msgid "Can't get parameters (%s)" msgstr "" #: frontends/src/primitivus/primitivus:816 msgid "Entering a MUC room" msgstr "" #: frontends/src/primitivus/primitivus:816 msgid "Please enter MUC's JID" msgstr "" #: frontends/src/primitivus/profile_manager.py:36 msgid "Login:" msgstr "" #: frontends/src/primitivus/profile_manager.py:37 msgid "Password:" msgstr "" #: frontends/src/primitivus/profile_manager.py:45 msgid "New" msgstr "" #: frontends/src/primitivus/profile_manager.py:46 msgid "Delete" msgstr "" #: frontends/src/primitivus/profile_manager.py:58 msgid "Profile Manager" msgstr "" #: frontends/src/primitivus/profile_manager.py:108 msgid "Can't create profile" msgstr "" #: frontends/src/primitivus/profile_manager.py:115 msgid "New profile" msgstr "" #: frontends/src/primitivus/profile_manager.py:115 msgid "Please enter a new profile name" msgstr "" #: frontends/src/primitivus/profile_manager.py:120 msgid "Are you sure you want to delete the profile {} ?" msgstr "" #: frontends/src/primitivus/progress.py:37 msgid "Clear progress list" msgstr "" #: frontends/src/primitivus/status.py:46 msgid "Set your presence" msgstr "" #: frontends/src/primitivus/status.py:52 msgid "Set your status" msgstr "" #: frontends/src/primitivus/status.py:52 msgid "New status" msgstr "" #: frontends/src/primitivus/xmlui.py:81 msgid "Unknown div_char" msgstr "" #: frontends/src/primitivus/xmlui.py:405 msgid "Submit" msgstr "" #: frontends/src/primitivus/xmlui.py:407 frontends/src/primitivus/xmlui.py:418 msgid "Cancel" msgstr "" #: frontends/src/primitivus/xmlui.py:409 msgid "OK" msgstr "" #: frontends/src/primitivus/xmlui.py:417 src/plugins/plugin_xep_0048.py:263 msgid "Save" msgstr "" #: frontends/src/quick_frontend/constants.py:27 #: src/plugins/plugin_xep_0050.py:54 msgid "Online" msgstr "" #: frontends/src/quick_frontend/constants.py:28 #: src/plugins/plugin_xep_0050.py:56 msgid "Free for chat" msgstr "" #: frontends/src/quick_frontend/constants.py:29 msgid "Away from keyboard" msgstr "" #: frontends/src/quick_frontend/constants.py:30 #: src/plugins/plugin_xep_0050.py:57 msgid "Do not disturb" msgstr "" #: frontends/src/quick_frontend/constants.py:31 msgid "Extended away" msgstr "" #: frontends/src/quick_frontend/quick_app.py:81 msgid "Error while trying to get autodisconnect param, ignoring: {}" msgstr "" #: frontends/src/quick_frontend/quick_app.py:174 #, python-brace-format msgid "Can't get profile parameter: {msg}" msgstr "" #: frontends/src/quick_frontend/quick_app.py:453 #, python-format msgid "Can't connect profile [%s]" msgstr "" #: frontends/src/quick_frontend/quick_app.py:510 msgid "Connected" msgstr "" #: frontends/src/quick_frontend/quick_app.py:517 msgid "Disconnected" msgstr "" #: frontends/src/quick_frontend/quick_app.py:574 #, python-format msgid "" "presence update for %(entity)s (show=%(show)s, priority=%(priority)s, " "statuses=%(statuses)s) [profile:%(profile)s]" msgstr "" #: frontends/src/quick_frontend/quick_app.py:783 #, python-format msgid "The contact %s has accepted your subscription" msgstr "" #: frontends/src/quick_frontend/quick_app.py:783 #: frontends/src/quick_frontend/quick_app.py:790 msgid "Subscription confirmation" msgstr "" #: frontends/src/quick_frontend/quick_app.py:786 #, python-format msgid "The contact %s has refused your subscription" msgstr "" #: frontends/src/quick_frontend/quick_app.py:786 msgid "Subscription refusal" msgstr "" #: frontends/src/quick_frontend/quick_app.py:790 #, python-format msgid "" "The contact %s wants to subscribe to your presence.\n" "Do you accept ?" msgstr "" #: frontends/src/quick_frontend/quick_app.py:813 #, python-format msgid "param update: [%(namespace)s] %(name)s = %(value)s" msgstr "" #: frontends/src/quick_frontend/quick_app.py:815 #, python-format msgid "Changing JID to %s" msgstr "" #: frontends/src/quick_frontend/quick_chat.py:434 msgid "now we print the history" msgstr "" #: frontends/src/quick_frontend/quick_chat.py:436 msgid " ({} messages)" msgstr "" #: frontends/src/quick_frontend/quick_chat.py:478 msgid "Can't get history: {}" msgstr "" #: frontends/src/quick_frontend/quick_contact_list.py:495 msgid "Trying to delete an unknow entity [{}]" msgstr "" #: frontends/src/quick_frontend/quick_contact_list.py:536 msgid "received presence from entity without resource: {}" msgstr "" #: frontends/src/quick_frontend/quick_contact_management.py:71 msgid "Trying to get attribute for an unknown contact" msgstr "" #: frontends/src/quick_frontend/quick_contact_management.py:87 msgid "INTERNAL ERROR: Key log.error" msgstr "" #: frontends/src/quick_frontend/quick_contact_management.py:99 #, python-format msgid "Trying to update an unknown contact: %s" msgstr "" #: frontends/src/quick_frontend/quick_games.py:74 #, python-brace-format msgid "" "A {game} activity between {players} has been started, but you couldn't take " "part because your client doesn't support it." msgstr "" #: frontends/src/quick_frontend/quick_games.py:75 #, python-brace-format msgid "{game} Game" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:116 msgid "Trying to plug an unknown profile key ({})" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:118 msgid "Profile plugging in error" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:130 msgid "Can't get profile parameter" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:141 msgid "A profile with this name already exists" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:143 msgid "Profile creation cancelled by backend" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:145 msgid "You profile name is not valid" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:147 msgid "Can't create profile ({})" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:163 #: src/stdui/ui_profile_manager.py:65 msgid "Internal error" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:163 msgid "You can't connect manually and automatically at the same time" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:168 msgid "No profile selected" msgstr "" #: frontends/src/quick_frontend/quick_profile_manager.py:168 msgid "You need to create and select at least one profile before connecting" msgstr "" #: frontends/src/quick_frontend/quick_utils.py:38 msgid "" "\n" " %prog [options]\n" "\n" " %prog --help for options list\n" " " msgstr "" #: frontends/src/quick_frontend/quick_utils.py:45 msgid "Select the profile to use" msgstr "" #: frontends/src/tools/xmlui.py:193 msgid "Nothing to submit" msgstr "" #: frontends/src/tools/xmlui.py:337 msgid "XMLUI can have only one main container" msgstr "" #: frontends/src/tools/xmlui.py:385 #, python-format msgid "Unknown container [%s], using default one" msgstr "" #: frontends/src/tools/xmlui.py:394 msgid "Internal Error, container has not _xmluiAppend method" msgstr "" #: frontends/src/tools/xmlui.py:472 #, python-format msgid "FIXME FIXME FIXME: widget type [%s] is not implemented" msgstr "" #: frontends/src/tools/xmlui.py:473 #, python-format msgid "FIXME FIXME FIXME: type [%s] is not implemented" msgstr "" #: frontends/src/tools/xmlui.py:481 #, python-format msgid "No change listener on [%s]" msgstr "" #: frontends/src/tools/xmlui.py:498 #, python-format msgid "Unknown tag [%s]" msgstr "" #: frontends/src/tools/xmlui.py:548 msgid "No callback_id found" msgstr "" #: frontends/src/tools/xmlui.py:580 #, python-format msgid "FIXME: XMLUI internal action [%s] is not implemented" msgstr "" #: frontends/src/tools/xmlui.py:667 frontends/src/tools/xmlui.py:677 msgid "The form data is not sent back, the type is not managed properly" msgstr "" #: frontends/src/tools/xmlui.py:672 msgid "Cancelling form" msgstr "" #: frontends/src/tools/xmlui.py:778 msgid "You must register classes with registerClass before creating a XMLUI" msgstr "" #: src/core/sat_main.py:149 msgid "Memory initialised" msgstr "" #: src/core/sat_main.py:154 msgid "Backend is ready" msgstr "" #: src/core/sat_main.py:189 #, python-brace-format msgid "" "Can't import plugin [{path}]:\n" "{error}" msgstr "" #: src/core/sat_main.py:202 #, python-brace-format msgid "{type} type must be used with {mode} mode, ignoring plugin" msgstr "" #: src/core/sat_main.py:208 #, python-brace-format msgid "" "Name conflict for import name [{import_name}], can't import plugin [{name}]" msgstr "" #: src/core/sat_main.py:234 msgid "Recommended plugin not found: {}" msgstr "" #: src/core/sat_main.py:248 #, python-brace-format msgid "Can't import plugin {name}: {error}" msgstr "" #: src/core/sat_main.py:311 msgid "already connected !" msgstr "" #: src/core/sat_main.py:331 msgid "not connected !" msgstr "" #: src/core/sat_main.py:406 msgid "Trying to remove reference to a client not referenced" msgstr "" #: src/core/sat_main.py:418 msgid "running app" msgstr "" #: src/core/sat_main.py:422 msgid "stopping app" msgstr "" #: src/core/sat_main.py:482 msgid "Unexpected error: {}" msgstr "" #: src/core/sat_main.py:498 msgid "asking connection status for a non-existant profile" msgstr "" #: src/core/sat_main.py:538 #, python-format msgid "subsciption request [%(subs_type)s] for %(jid)s" msgstr "" #: src/core/sat_main.py:654 msgid "Trying to remove an unknow progress callback" msgstr "" #: src/core/sat_main.py:738 msgid "id already registered" msgstr "" #: src/core/sat_main.py:776 msgid "trying to launch action with a non-existant profile" msgstr "" #: src/core/sat_main.py:858 msgid "A menu with the same path and type already exists" msgstr "" #: src/core/xmpp.py:120 msgid "Can't parse port value, using default value" msgstr "" #: src/core/xmpp.py:141 msgid "setting plugins parents" msgstr "" #: src/core/xmpp.py:160 msgid "Plugins initialisation error" msgstr "" #: src/core/xmpp.py:177 msgid "Error while disconnecting: {}" msgstr "" #: src/core/xmpp.py:191 #, python-format msgid "********** [%s] CONNECTED **********" msgstr "" #: src/core/xmpp.py:200 msgid "XML stream is initialized" msgstr "" #: src/core/xmpp.py:216 #, python-format msgid "ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" msgstr "" #: src/core/xmpp.py:230 msgid "No keep_alife" msgstr "" #: src/core/xmpp.py:235 #, python-format msgid "********** [%s] DISCONNECTED **********" msgstr "" #: src/core/xmpp.py:257 msgid "Disconnecting..." msgstr "" #: src/core/xmpp.py:401 #, python-brace-format msgid "Sending message (type {type}, to {to})" msgstr "" #: src/core/xmpp.py:407 msgid "" "Triggers, storage and echo have been inhibited by the 'send_only' parameter" msgstr "" #: src/core/xmpp.py:447 msgid "No message found" msgstr "" #: src/core/xmpp.py:581 #, python-brace-format msgid "The requested entry point ({entry_point}) is not available" msgstr "" #: src/core/xmpp.py:615 #, python-brace-format msgid "" "Plugin {current_name} if needed for {entry_name}, but it doesn't handle " "component mode" msgstr "" #: src/core/xmpp.py:619 msgid "invalid plugin mode" msgstr "" #: src/core/xmpp.py:717 #, python-brace-format msgid "got message from: {from_}" msgstr "" #: src/core/xmpp.py:793 msgid "There's no subscription between you and [{}]!" msgstr "" #: src/core/xmpp.py:795 msgid "You are not subscribed to [{}]!" msgstr "" #: src/core/xmpp.py:797 msgid "[{}] is not subscribed to you!" msgstr "" #: src/core/xmpp.py:954 #, python-format msgid "" "presence update for [%(entity)s] (available, show=%(show)s statuses=" "%(statuses)s priority=%(priority)d)" msgstr "" #: src/core/xmpp.py:975 #, python-format msgid "presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)" msgstr "" #: src/core/xmpp.py:1038 msgid "sending automatic \"from\" subscription request" msgstr "" #: src/core/xmpp.py:1046 #, python-format msgid "subscription approved for [%s]" msgstr "" #: src/core/xmpp.py:1050 #, python-format msgid "unsubscription confirmed for [%s]" msgstr "" #: src/core/xmpp.py:1055 #, python-format msgid "subscription request from [%s]" msgstr "" #: src/core/xmpp.py:1060 msgid "sending automatic subscription acceptance" msgstr "" #: src/core/xmpp.py:1068 #, python-format msgid "unsubscription asked for [%s]" msgstr "" #: src/core/xmpp.py:1072 msgid "automatic contact deletion" msgstr "" #: src/core/xmpp.py:1104 #, python-format msgid "Registration asked for %(user)s@%(host)s" msgstr "" #: src/core/xmpp.py:1107 #, python-format msgid "Connection made with %s" msgstr "" #: src/core/xmpp.py:1125 #, python-format msgid "Registration answer: %s" msgstr "" #: src/core/xmpp.py:1129 src/plugins/plugin_xep_0077.py:87 #: src/plugins/plugin_xep_0077.py:97 #, python-format msgid "Registration failure: %s" msgstr "" #: src/memory/disco.py:219 #, python-format msgid "Error while requesting [%(jid)s]: %(error)s" msgstr "" #: src/memory/disco.py:307 #, python-format msgid "Capability hash generated: [%s]" msgstr "" #: src/memory/disco.py:348 msgid "invalid item (no jid)" msgstr "" #: src/memory/memory.py:219 msgid "" "A database has been found in the default local_dir for previous versions (< " "0.5)" msgstr "" #: src/memory/memory.py:227 msgid "Memory manager init" msgstr "" #: src/memory/memory.py:244 msgid "Loading default params template" msgstr "" #: src/memory/memory.py:276 #, python-format msgid "Parameters loaded from file: %s" msgstr "" #: src/memory/memory.py:279 #, python-format msgid "Can't load parameters from file: %s" msgstr "" #: src/memory/memory.py:294 #, python-format msgid "Parameters saved to file: %s" msgstr "" #: src/memory/memory.py:297 #, python-format msgid "Can't save parameters to file: %s" msgstr "" #: src/memory/memory.py:426 #, python-format msgid "[%s] Profile session purge" msgstr "" #: src/memory/memory.py:431 #, python-format msgid "Trying to purge roster status cache for a profile not in memory: [%s]" msgstr "" #: src/memory/memory.py:441 msgid "requesting no profiles at all" msgstr "" #: src/memory/memory.py:496 #, python-brace-format msgid "Can't find component {component} entry point" msgstr "" #: src/memory/memory.py:740 msgid "Trying to get entity data for a non-existant profile" msgstr "" #: src/memory/memory.py:929 msgid "Need a bare jid to delete all resources" msgstr "" #: src/memory/memory.py:954 #, python-format msgid "Trying to encrypt a value for %s while the personal key is undefined!" msgstr "" #: src/memory/memory.py:969 #, python-format msgid "Trying to decrypt a value for %s while the personal key is undefined!" msgstr "" #: src/memory/memory.py:992 #, python-format msgid "Personal data (%(ns)s, %(key)s) has been successfuly encrypted" msgstr "" #: src/memory/memory.py:1019 msgid "Asking waiting subscriptions for a non-existant profile" msgstr "" #: src/memory/params.py:82 msgid "Connection" msgstr "" #: src/memory/params.py:84 msgid "Chat history limit" msgstr "" #: src/memory/params.py:86 msgid "Show offline contacts" msgstr "" #: src/memory/params.py:88 msgid "Show empty groups" msgstr "" #: src/memory/params.py:91 src/plugins/plugin_misc_register_account.py:74 msgid "Register new account" msgstr "" #: src/memory/params.py:92 msgid "Connect on frontend startup" msgstr "" #: src/memory/params.py:93 msgid "Disconnect on frontend closure" msgstr "" #: src/memory/params.py:154 #, python-format msgid "Trying to purge cache of a profile not in memory: [%s]" msgstr "" #: src/memory/params.py:178 msgid "The profile name already exists" msgstr "" #: src/memory/params.py:193 msgid "Trying to delete an unknown profile" msgstr "" #: src/memory/params.py:199 msgid "Trying to delete a connected profile" msgstr "" #: src/memory/params.py:217 msgid "No default profile, returning first one" msgstr "" #: src/memory/params.py:221 msgid "No profile exist yet" msgstr "" #: src/memory/params.py:229 #, python-format msgid "Trying to access an unknown profile (%s)" msgstr "" #: src/memory/params.py:314 msgid "Trying to register frontends parameters with no specified app: aborted" msgstr "" #: src/memory/params.py:319 #, python-format msgid "Trying to register twice frontends parameters for %(app)s: aborted" msgstr "" #: src/memory/params.py:330 #, python-format msgid "Can't determine default value for [%(category)s/%(name)s]: %(reason)s" msgstr "" #: src/memory/params.py:346 src/memory/params.py:478 src/memory/params.py:523 #, python-format msgid "Requested param [%(name)s] in category [%(category)s] doesn't exist !" msgstr "" #: src/memory/params.py:381 #, python-format msgid "" "Unset parameter (%(cat)s, %(param)s) of type list will use the default " "option '%(value)s'" msgstr "" #: src/memory/params.py:385 #, python-format msgid "Parameter (%(cat)s, %(param)s) of type list has no default option!" msgstr "" #: src/memory/params.py:387 #, python-format msgid "" "Parameter (%(cat)s, %(param)s) of type list has more than one default option!" msgstr "" #: src/memory/params.py:444 msgid "The stored password could not be decrypted!" msgstr "" #: src/memory/params.py:494 msgid "Requesting a param for an non-existant profile" msgstr "" #: src/memory/params.py:498 msgid "Requesting synchronous param for not connected profile" msgstr "" #: src/memory/params.py:527 #, python-format msgid "" "Trying to get parameter '%(param)s' in category '%(cat)s' without " "authorization!!!" msgstr "" #: src/memory/params.py:539 msgid "Requesting a param for a non-existant profile" msgstr "" #: src/memory/params.py:564 src/memory/params.py:742 src/memory/params.py:759 msgid "Asking params for inexistant profile" msgstr "" #: src/memory/params.py:817 msgid "Trying to set parameter for an unknown profile" msgstr "" #: src/memory/params.py:822 #, python-format msgid "Requesting an unknown parameter (%(category)s/%(name)s)" msgstr "" #: src/memory/params.py:827 #, python-format msgid "" "Trying to set parameter '%(param)s' in category '%(cat)s' without " "authorization!!!" msgstr "" #: src/memory/params.py:839 #, python-format msgid "" "Trying to set parameter '%(param)s' in category '%(cat)s' with an non-" "integer value" msgstr "" #: src/memory/params.py:851 #, python-format msgid "Setting parameter (%(category)s, %(name)s) = %(value)s" msgstr "" #: src/memory/params.py:873 msgid "Trying to encrypt a password while the personal key is undefined!" msgstr "" #: src/memory/persistent.py:39 msgid "PersistentDict can't be used before memory initialisation" msgstr "" #: src/memory/sqlite.py:127 msgid "Connecting database" msgstr "" #: src/memory/sqlite.py:139 msgid "Can't activate foreign keys" msgstr "" #: src/memory/sqlite.py:142 msgid "The database is new, creating the tables" msgstr "" #: src/memory/sqlite.py:251 #, python-format msgid "Can't delete profile [%s]" msgstr "" #: src/memory/sqlite.py:269 #, python-format msgid "Profile [%s] deleted" msgstr "" #: src/memory/sqlite.py:285 msgid "loading general parameters from database" msgstr "" #: src/memory/sqlite.py:300 msgid "loading individual parameters from database" msgstr "" #: src/memory/sqlite.py:324 #, python-format msgid "Can't set general parameter (%(category)s/%(name)s) in database" msgstr "" #: src/memory/sqlite.py:337 #, python-format msgid "" "Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] in " "database" msgstr "" #: src/memory/sqlite.py:350 #, python-brace-format msgid "" "Can't save following {key} in history (uid: {uid}, lang:{lang}): {value}" msgstr "" #: src/memory/sqlite.py:360 #, python-brace-format msgid "" "Can't save following thread in history (uid: {uid}): thread:{thread}), " "parent:{parent}" msgstr "" #: src/memory/sqlite.py:379 #, python-brace-format msgid "" "Can't save following message in history: from [{from_jid}] to [{to_jid}] " "(uid: {uid})" msgstr "" #: src/memory/sqlite.py:535 #, python-format msgid "loading general private values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:537 src/memory/sqlite.py:555 #, python-format msgid "No data present in database for namespace %s" msgstr "" #: src/memory/sqlite.py:552 #, python-format msgid "loading individual private values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:566 #, python-format msgid "" "Can't set general private value (%(key)s) [namespace:%(namespace)s] in " "database" msgstr "" #: src/memory/sqlite.py:580 #, python-format msgid "" "Can't set individual private value (%(key)s) [namespace: %(namespace)s] for " "[%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:592 #, python-format msgid "" "Can't delete general private value (%(key)s) [namespace:%(namespace)s] in " "database" msgstr "" #: src/memory/sqlite.py:605 #, python-format msgid "" "Can't delete individual private value (%(key)s) [namespace: %(namespace)s] " "for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:621 #, python-format msgid "loading general private binary values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:623 src/memory/sqlite.py:641 #, python-format msgid "No binary data present in database for namespace %s" msgstr "" #: src/memory/sqlite.py:638 #, python-format msgid "loading individual private binary values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:652 #, python-format msgid "" "Can't set general private binary value (%(key)s) [namespace:%(namespace)s] " "in database" msgstr "" #: src/memory/sqlite.py:666 #, python-format msgid "" "Can't set individual binary private value (%(key)s) [namespace: " "%(namespace)s] for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:678 #, python-format msgid "" "Can't delete general private binary value (%(key)s) [namespace:" "%(namespace)s] in database" msgstr "" #: src/memory/sqlite.py:691 #, python-format msgid "" "Can't delete individual private binary value (%(key)s) [namespace: " "%(namespace)s] for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:761 msgid "" "Your local schema is up-to-date, but database versions mismatch, fixing it..." msgstr "" #: src/memory/sqlite.py:771 msgid "" "There is a schema mismatch, but as we are on a dev version, database will be " "updated" msgstr "" #: src/memory/sqlite.py:775 msgid "" "schema version is up-to-date, but local schema differ from expected current " "schema" msgstr "" #: src/memory/sqlite.py:778 #, 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 "" #: src/memory/sqlite.py:783 msgid "" "Database content needs a specific processing, local database will be updated" msgstr "" #: src/memory/sqlite.py:785 msgid "Database schema has changed, local database will be updated" msgstr "" #: src/plugins/plugin_adhoc_dbus.py:53 msgid "Add D-Bus management to Ad-Hoc commands" msgstr "" #: src/plugins/plugin_adhoc_dbus.py:60 msgid "plugin Ad-Hoc D-Bus initialization" msgstr "" #: src/plugins/plugin_adhoc_dbus.py:169 msgid "Command selection" msgstr "" #: src/plugins/plugin_adhoc_dbus.py:206 src/plugins/plugin_xep_0050.py:439 msgid "Updated" msgstr "" #: src/plugins/plugin_adhoc_dbus.py:210 msgid "Command sent" msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:52 msgid "Blog importer for Dokuwiki blog engine." msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:55 msgid "import posts from Dokuwiki blog engine" msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:57 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 "" #: src/plugins/plugin_blog_import_dokuwiki.py:341 msgid "plugin Dokuwiki Import initialization" msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:371 #, python-brace-format msgid "" "DokuWiki media files will be *downloaded* to {temp_dir} - to finish the " "import you have to upload them *manually* to {media_repo}" msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:375 msgid "" "DokuWiki media files will be *uploaded* to the XMPP server. Hyperlinks to " "these media may not been updated though." msgstr "" #: src/plugins/plugin_blog_import_dokuwiki.py:377 #, python-brace-format msgid "" "DokuWiki media files will *stay* on {location} - some of them may be " "protected by DokuWiki ACL and will not be accessible." msgstr "" #: src/plugins/plugin_blog_import_dotclear.py:41 msgid "Blog importer for Dotclear blog engine." msgstr "" #: src/plugins/plugin_blog_import_dotclear.py:44 msgid "import posts from Dotclear blog engine" msgstr "" #: src/plugins/plugin_blog_import_dotclear.py:46 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 "" #: src/plugins/plugin_blog_import_dotclear.py:242 msgid "plugin Dotclear Import initialization" msgstr "" #: src/plugins/plugin_blog_import.py:44 msgid "" "Blog import management:\n" "This plugin manage the different blog importers which can register to it, " "and handler generic importing tasks." msgstr "" #: src/plugins/plugin_blog_import.py:62 msgid "plugin Blog Import initialization" msgstr "" #: src/plugins/plugin_comp_ep_test.py:36 src/plugins/plugin_exp_parrot.py:38 msgid "Implementation of parrot mode (repeat messages between 2 entities)" msgstr "" #: src/plugins/plugin_comp_ep_test.py:44 msgid "Plugin Test initialization" msgstr "" #: src/plugins/plugin_exp_command_export.py:38 msgid "Implementation of command export" msgstr "" #: src/plugins/plugin_exp_command_export.py:89 msgid "Plugin command export initialization" msgstr "" #: src/plugins/plugin_exp_lang_detect.py:42 msgid "Detect and set message language when unknown" msgstr "" #: src/plugins/plugin_exp_lang_detect.py:45 #: src/plugins/plugin_misc_watched.py:40 src/plugins/plugin_xep_0249.py:73 msgid "Misc" msgstr "" #: src/plugins/plugin_exp_lang_detect.py:47 msgid "language detection" msgstr "" #: src/plugins/plugin_exp_lang_detect.py:65 msgid "Language detection plugin initialization" msgstr "" #: src/plugins/plugin_exp_parrot.py:51 msgid "Plugin Parrot initialization" msgstr "" #: src/plugins/plugin_exp_parrot.py:58 src/plugins/plugin_xep_0045.py:99 #: src/plugins/plugin_xep_0048.py:73 src/plugins/plugin_xep_0092.py:54 #: src/plugins/plugin_xep_0249.py:89 msgid "Text commands not available" msgstr "" #: src/plugins/plugin_exp_pipe.py:40 msgid "Jingle Pipe Transfer experimental plugin" msgstr "" #: src/plugins/plugin_exp_pipe.py:43 #, python-brace-format msgid "{peer} wants to send you a pipe stream, do you accept ?" msgstr "" #: src/plugins/plugin_exp_pipe.py:44 msgid "Pipe stream" msgstr "" #: src/plugins/plugin_exp_pipe.py:50 msgid "Plugin Pipe initialization" msgstr "" #: src/plugins/plugin_misc_account.py:48 msgid "SàT account creation" msgstr "" #: src/plugins/plugin_misc_account.py:105 msgid "Prosody command succeed" msgstr "" #: src/plugins/plugin_misc_account.py:108 #, python-format msgid "Can't complete Prosody command (error code: %(code)d): %(message)s" msgstr "" #: src/plugins/plugin_misc_account.py:135 msgid "Plugin Account initialization" msgstr "" #: src/plugins/plugin_misc_account.py:145 #, python-format msgid "Can't find %s" msgstr "" #: src/plugins/plugin_misc_account.py:148 #, python-format msgid "Prosody path found: %s" msgstr "" #: src/plugins/plugin_misc_account.py:314 msgid "New Libervia account created" msgstr "" #: src/plugins/plugin_misc_account.py:326 #, python-brace-format msgid "" "Welcome to Libervia, the web interface of Salut à Toi.\n" "\n" "Your account on {domain} has been successfully created. This is a " "demonstration version to show you the current status of the project. 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" "http://www.salut-a-toi.org\n" msgstr "" #: src/plugins/plugin_misc_account.py:346 msgid "Libervia account created" msgstr "" #: src/plugins/plugin_misc_account.py:372 msgid "Manage your account" msgstr "" #: src/plugins/plugin_misc_account.py:375 msgid "Change your password" msgstr "" #: src/plugins/plugin_misc_account.py:376 msgid "Current profile password" msgstr "" #: src/plugins/plugin_misc_account.py:378 msgid "New password" msgstr "" #: src/plugins/plugin_misc_account.py:380 msgid "New password (again)" msgstr "" #: src/plugins/plugin_misc_account.py:418 src/stdui/ui_profile_manager.py:62 msgid "The provided profile password doesn't match." msgstr "" #: src/plugins/plugin_misc_account.py:419 msgid "Attempt failure" msgstr "" #: src/plugins/plugin_misc_account.py:462 msgid "The values entered for the new password are not equal." msgstr "" #: src/plugins/plugin_misc_account.py:474 msgid "Change your password?" msgstr "" #: src/plugins/plugin_misc_account.py:475 msgid "" "Note for advanced users: this will actually change both your SàT profile " "password AND your XMPP account password." msgstr "" #: src/plugins/plugin_misc_account.py:476 msgid "Continue with changing the password?" msgstr "" #: src/plugins/plugin_misc_account.py:490 #: src/plugins/plugin_misc_register_account.py:108 msgid "Confirmation" msgstr "" #: src/plugins/plugin_misc_account.py:491 msgid "Your password has been changed." msgstr "" #: src/plugins/plugin_misc_account.py:496 #, python-format msgid "Your password could not be changed: %s" msgstr "" #: src/plugins/plugin_misc_account.py:507 msgid "Delete your account?" msgstr "" #: src/plugins/plugin_misc_account.py:508 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 "" #: src/plugins/plugin_misc_account.py:509 msgid "contact list and messages history" msgstr "" #: src/plugins/plugin_misc_account.py:509 msgid "contact list, messages history, blog posts and comments" msgstr "" #: src/plugins/plugin_misc_account.py:510 #, python-format msgid "" "All your data stored on %(server)s, including your %(target)s will be erased." msgstr "" #: src/plugins/plugin_misc_account.py:511 #: src/plugins/plugin_misc_account.py:558 #: src/plugins/plugin_misc_account.py:562 #: src/plugins/plugin_misc_account.py:566 msgid "" "There is no other confirmation dialog, this is the very last one! Are you " "sure?" msgstr "" #: src/plugins/plugin_misc_account.py:539 #, python-format msgid "Your XMPP account could not be deleted: %s" msgstr "" #: src/plugins/plugin_misc_account.py:555 msgid "Delete all your (micro-)blog posts and comments?" msgstr "" #: src/plugins/plugin_misc_account.py:556 msgid "" "If you confirm this dialog, all the (micro-)blog data you submitted will be " "erased." msgstr "" #: src/plugins/plugin_misc_account.py:557 msgid "" "These are the public and private posts and comments you sent to any group." msgstr "" #: src/plugins/plugin_misc_account.py:560 msgid "Delete all your (micro-)blog posts?" msgstr "" #: src/plugins/plugin_misc_account.py:561 msgid "" "If you confirm this dialog, all the public and private posts you sent to any " "group will be erased." msgstr "" #: src/plugins/plugin_misc_account.py:564 msgid "Delete all your (micro-)blog comments?" msgstr "" #: src/plugins/plugin_misc_account.py:565 msgid "" "If you confirm this dialog, all the public and private comments you made on " "other people's posts will be erased." msgstr "" #: src/plugins/plugin_misc_account.py:578 msgid "blog posts and comments" msgstr "" #: src/plugins/plugin_misc_account.py:581 msgid "blog posts" msgstr "" #: src/plugins/plugin_misc_account.py:584 msgid "comments" msgstr "" #: src/plugins/plugin_misc_account.py:588 msgid "Deletion confirmation" msgstr "" #: src/plugins/plugin_misc_account.py:590 #, python-format msgid "Your %(target)s have been deleted." msgstr "" #: src/plugins/plugin_misc_account.py:591 msgid "" "Known issue of the demo version: you need to refresh the page to make the " "deleted posts actually disappear." msgstr "" #: src/plugins/plugin_misc_account.py:596 #, python-format msgid "Your %(target)s could not be deleted: %(message)s" msgstr "" #: src/plugins/plugin_misc_android.py:35 msgid "Manage Android platform specificities, like pause or notifications" msgstr "" #: src/plugins/plugin_misc_android.py:45 msgid "Vibrate on notifications" msgstr "" #: src/plugins/plugin_misc_android.py:65 msgid "plugin Android initialization" msgstr "" #: src/plugins/plugin_misc_debug.py:34 msgid "Set of method to make development and debugging easier" msgstr "" #: src/plugins/plugin_misc_debug.py:41 msgid "Plugin Debug initialization" msgstr "" #: src/plugins/plugin_misc_extra_pep.py:37 msgid "Display messages from extra PEP services" msgstr "" #: src/plugins/plugin_misc_extra_pep.py:68 msgid "Plugin Extra PEP initialization" msgstr "" #: src/plugins/plugin_misc_file.py:39 msgid "" "File Tansfer Management:\n" "This plugin manage the various ways of sending a file, and choose the best " "one." msgstr "" #: src/plugins/plugin_misc_file.py:44 #, python-brace-format msgid "Please select a file to send to {peer}" msgstr "" #: src/plugins/plugin_misc_file.py:45 msgid "File sending" msgstr "" #: src/plugins/plugin_misc_file.py:46 #, python-brace-format 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 "" #: src/plugins/plugin_misc_file.py:47 msgid "Confirm file transfer" msgstr "" #: src/plugins/plugin_misc_file.py:48 msgid "File {} already exists, are you sure you want to overwrite ?" msgstr "" #: src/plugins/plugin_misc_file.py:49 msgid "File exists" msgstr "" #: src/plugins/plugin_misc_file.py:194 msgid "plugin File initialization" msgstr "" #: src/plugins/plugin_misc_file.py:198 msgid "Action" msgstr "" #: src/plugins/plugin_misc_file.py:198 msgid "send file" msgstr "" #: src/plugins/plugin_misc_file.py:198 msgid "Send a file" msgstr "" #: src/plugins/plugin_misc_file.py:244 src/plugins/plugin_xep_0100.py:77 msgid "Invalid JID" msgstr "" #: src/plugins/plugin_misc_groupblog.py:51 msgid "Implementation of microblogging fine permissions" msgstr "" #: src/plugins/plugin_misc_groupblog.py:59 msgid "Group blog plugin initialization" msgstr "" #: src/plugins/plugin_misc_groupblog.py:77 msgid "" "Server is not able to manage item-access pubsub, we can't use group blog" msgstr "" #: src/plugins/plugin_misc_groupblog.py:80 msgid "Server can manage group blogs" msgstr "" #: src/plugins/plugin_misc_imap.py:44 msgid "" "Create an Imap server that you can use to read your \"normal\" type messages" msgstr "" #: src/plugins/plugin_misc_imap.py:62 msgid "Plugin Imap Server initialization" msgstr "" #: src/plugins/plugin_misc_imap.py:69 #, python-format msgid "Launching IMAP server on port %d" msgstr "" #: src/plugins/plugin_misc_imap.py:437 msgid "IMAP server connection started" msgstr "" #: src/plugins/plugin_misc_imap.py:440 #, python-format msgid "IMAP server connection lost (reason: %s)" msgstr "" #: src/plugins/plugin_misc_ip.py:52 msgid "This plugin help to discover our external IP address." msgstr "" #: src/plugins/plugin_misc_ip.py:57 msgid "Allow external get IP" msgstr "" #: src/plugins/plugin_misc_ip.py:60 msgid "Confirm external site request" msgstr "" #: src/plugins/plugin_misc_ip.py:61 #, python-brace-format 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 "" #: src/plugins/plugin_misc_ip.py:90 msgid "plugin IP discovery initialization" msgstr "" #: src/plugins/plugin_misc_maildir.py:42 msgid "Intercept \"normal\" type messages, and put them in a Maildir type box" msgstr "" #: src/plugins/plugin_misc_maildir.py:46 msgid "Mail Server" msgstr "" #: src/plugins/plugin_misc_maildir.py:47 msgid "Block \"normal\" messages propagation" msgstr "" #: src/plugins/plugin_misc_maildir.py:71 msgid "Plugin Maildir initialization" msgstr "" #: src/plugins/plugin_misc_maildir.py:141 msgid "Trying to remove an mailboxUser not referenced" msgstr "" #: src/plugins/plugin_misc_maildir.py:142 #: src/plugins/plugin_misc_maildir.py:161 #: src/plugins/plugin_misc_maildir.py:300 #: src/plugins/plugin_misc_maildir.py:304 #: src/plugins/plugin_misc_maildir.py:308 msgid "INTERNAL ERROR: " msgstr "" #: src/plugins/plugin_misc_maildir.py:160 msgid "Boxname doesn't exist in internal data" msgstr "" #: src/plugins/plugin_misc_maildir.py:299 msgid "Trying to remove an observer for an inexistant mailbox" msgstr "" #: src/plugins/plugin_misc_maildir.py:303 msgid "Trying to remove an inexistant observer, no observer for this signal" msgstr "" #: src/plugins/plugin_misc_maildir.py:307 msgid "Trying to remove an inexistant observer" msgstr "" #: src/plugins/plugin_misc_nat-port.py:42 msgid "Automatic NAT port mapping using UPnP" msgstr "" #: src/plugins/plugin_misc_nat-port.py:57 msgid "plugin NAT Port initialization" msgstr "" #: src/plugins/plugin_misc_quiz.py:41 msgid "Implementation of Quiz game" msgstr "" #: src/plugins/plugin_misc_quiz.py:53 msgid "Plugin Quiz initialization" msgstr "" #: src/plugins/plugin_misc_quiz.py:253 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 "" #: src/plugins/plugin_misc_quiz.py:282 src/plugins/plugin_misc_tarot.py:507 #, python-format msgid "Player %(player)s is ready to start [status: %(status)s]" msgstr "" #: src/plugins/plugin_misc_quiz.py:330 src/plugins/plugin_misc_radiocol.py:252 #, python-format msgid "Unmanaged game element: %s" msgstr "" #: src/plugins/plugin_misc_radiocol.py:53 msgid "Implementation of radio collective" msgstr "" #: src/plugins/plugin_misc_radiocol.py:71 msgid "Radio collective initialization" msgstr "" #: src/plugins/plugin_misc_radiocol.py:127 msgid "" "The uploaded file has been rejected, only Ogg Vorbis and MP3 songs are " "accepted." msgstr "" #: src/plugins/plugin_misc_radiocol.py:146 msgid "No more participants in the radiocol: cleaning data" msgstr "" #: src/plugins/plugin_misc_radiocol.py:182 msgid "INTERNAL ERROR: can't find full path of the song to delete" msgstr "" #: src/plugins/plugin_misc_radiocol.py:189 #, python-format msgid "INTERNAL ERROR: can't find %s on the file system" msgstr "" #: src/plugins/plugin_misc_register_account.py:42 msgid "Register XMPP account" msgstr "" #: src/plugins/plugin_misc_register_account.py:49 msgid "Plugin Register Account initialization" msgstr "" #: src/plugins/plugin_misc_register_account.py:67 msgid "Missing values" msgstr "" #: src/plugins/plugin_misc_register_account.py:68 msgid "No user JID or password given: can't register new account." msgstr "" #: src/plugins/plugin_misc_register_account.py:75 #, python-format msgid "" "Do you want to register a new XMPP account [%(user)s] on server %(server)s ?" msgstr "" #: src/plugins/plugin_misc_register_account.py:109 msgid "Registration successful." msgstr "" #: src/plugins/plugin_misc_register_account.py:113 msgid "Failure" msgstr "" #: src/plugins/plugin_misc_register_account.py:114 #, python-format msgid "Registration failed: %s" msgstr "" #: src/plugins/plugin_misc_register_account.py:117 msgid "Username already exists, please choose an other one." msgstr "" #: src/plugins/plugin_misc_room_game.py:47 msgid "Base class for MUC games" msgstr "" #: src/plugins/plugin_misc_room_game.py:212 #, python-format msgid "%(user)s not allowed to join the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:348 #, python-format msgid "%(user)s not allowed to invite for the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:394 #, python-format msgid "" "Still waiting for %(users)s before starting the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:423 #, python-format msgid "Preparing room for %s game" msgstr "" #: src/plugins/plugin_misc_room_game.py:426 msgid "Unknown profile" msgstr "" #: src/plugins/plugin_misc_room_game.py:521 #, python-format msgid "%(game)s game already created in room %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:524 #, python-format msgid "%(game)s game in room %(room)s can only be created by %(user)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:542 #, python-format msgid "Creating %(game)s game in room %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:545 #: src/plugins/plugin_misc_room_game.py:574 #: src/plugins/plugin_misc_tarot.py:438 #, python-format msgid "profile %s is unknown" msgstr "" #: src/plugins/plugin_misc_room_game.py:589 #, python-format msgid "new round for %s game" msgstr "" #: src/plugins/plugin_misc_smtp.py:45 msgid "" "Create a SMTP server that you can use to send your \"normal\" type messages" msgstr "" #: src/plugins/plugin_misc_smtp.py:62 msgid "Plugin SMTP Server initialization" msgstr "" #: src/plugins/plugin_misc_smtp.py:69 #, python-format msgid "Launching SMTP server on port %d" msgstr "" #: src/plugins/plugin_misc_smtp.py:95 #, python-format msgid "Can't send message: %s" msgstr "" #: src/plugins/plugin_misc_smtp.py:200 msgid "SMTP server connection started" msgstr "" #: src/plugins/plugin_misc_smtp.py:204 #, python-format msgid "SMTP server connection lost (reason: %s)" msgstr "" #: src/plugins/plugin_misc_static_blog.py:41 msgid "Plugin for static blogs" msgstr "" #: src/plugins/plugin_misc_static_blog.py:62 msgid "Page title" msgstr "" #: src/plugins/plugin_misc_static_blog.py:64 msgid "Banner URL" msgstr "" #: src/plugins/plugin_misc_static_blog.py:66 msgid "Keywords" msgstr "" #: src/plugins/plugin_misc_static_blog.py:68 msgid "Description" msgstr "" #: src/plugins/plugin_misc_static_blog.py:91 src/plugins/plugin_sec_otr.py:290 #: src/plugins/plugin_sec_otr.py:316 src/plugins/plugin_sec_otr.py:339 #: src/plugins/plugin_sec_otr.py:418 msgid "jid key is not present !" msgstr "" #: src/plugins/plugin_misc_static_blog.py:96 msgid "Not available" msgstr "" #: src/plugins/plugin_misc_static_blog.py:97 msgid "Retrieving a blog from an external domain is not implemented yet." msgstr "" #: src/plugins/plugin_misc_tarot.py:46 msgid "Implementation of Tarot card game" msgstr "" #: src/plugins/plugin_misc_tarot.py:58 msgid "Plugin Tarot initialization" msgstr "" #: src/plugins/plugin_misc_tarot.py:64 msgid "Passe" msgstr "" #: src/plugins/plugin_misc_tarot.py:64 msgid "Petite" msgstr "" #: src/plugins/plugin_misc_tarot.py:64 msgid "Garde" msgstr "" #: src/plugins/plugin_misc_tarot.py:64 msgid "Garde Sans" msgstr "" #: src/plugins/plugin_misc_tarot.py:64 msgid "Garde Contre" msgstr "" #: src/plugins/plugin_misc_tarot.py:107 msgid "contrat selection" msgstr "" #: src/plugins/plugin_misc_tarot.py:120 msgid "scores" msgstr "" #: src/plugins/plugin_misc_tarot.py:201 src/plugins/plugin_misc_tarot.py:232 #, python-format msgid "" "Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for " "Excuse compensation" msgstr "" #: src/plugins/plugin_misc_tarot.py:237 #, python-format msgid "" "%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is " "waiting for one" msgstr "" #: src/plugins/plugin_misc_tarot.py:247 src/plugins/plugin_misc_tarot.py:319 #, python-format msgid "" "\n" "--\n" "%(player)s:\n" "score for this game ==> %(score_game)i\n" "total score ==> %(total_score)i" msgstr "" #: src/plugins/plugin_misc_tarot.py:297 msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" msgstr "" #: src/plugins/plugin_misc_tarot.py:316 #, 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 "" #: src/plugins/plugin_misc_tarot.py:377 msgid "Internal error: unmanaged game stage" msgstr "" #: src/plugins/plugin_misc_tarot.py:396 src/plugins/plugin_misc_tarot.py:419 msgid "session id doesn't exist, session has probably expired" msgstr "" #: src/plugins/plugin_misc_tarot.py:405 #, python-format msgid "contrat [%(contrat)s] choosed by %(profile)s" msgstr "" #: src/plugins/plugin_misc_tarot.py:440 #, python-format msgid "Cards played by %(profile)s: [%(cards)s]" msgstr "" #: src/plugins/plugin_misc_tarot.py:542 msgid "Everybody is passing, round ended" msgstr "" #: src/plugins/plugin_misc_tarot.py:549 #, python-format msgid "%(player)s win the bid with %(contrat)s" msgstr "" #: src/plugins/plugin_misc_tarot.py:570 msgid "tarot: chien received" msgstr "" #: src/plugins/plugin_misc_tarot.py:621 #, python-format msgid "The winner of this trick is %s" msgstr "" #: src/plugins/plugin_misc_tarot.py:667 #, python-format msgid "Unmanaged error type: %s" msgstr "" #: src/plugins/plugin_misc_tarot.py:669 #, python-format msgid "Unmanaged card game element: %s" msgstr "" #: src/plugins/plugin_misc_text_commands.py:38 msgid "IRC like text commands" msgstr "" #: src/plugins/plugin_misc_text_commands.py:57 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 "" #: src/plugins/plugin_misc_text_commands.py:60 msgid "Text commands initialization" msgstr "" #: src/plugins/plugin_misc_text_commands.py:143 #, python-format msgid "Skipping not callable [%s] attribute" msgstr "" #: src/plugins/plugin_misc_text_commands.py:147 msgid "Skipping cmd_ method" msgstr "" #: src/plugins/plugin_misc_text_commands.py:153 #, python-brace-format msgid "Conflict for command [{old_name}], renaming it to [{new_name}]" msgstr "" #: src/plugins/plugin_misc_text_commands.py:157 #, python-format msgid "Registered text command [%s]" msgstr "" #: src/plugins/plugin_misc_text_commands.py:236 #, python-format msgid "Unknown command /%s. " msgstr "" #: src/plugins/plugin_misc_text_commands.py:242 msgid "group discussions" msgstr "" #: src/plugins/plugin_misc_text_commands.py:242 msgid "one to one discussions" msgstr "" #: src/plugins/plugin_misc_text_commands.py:243 #, python-brace-format msgid "/{command} command only applies in {context}." msgstr "" #: src/plugins/plugin_misc_text_commands.py:322 msgid "Invalid jid, can't whois" msgstr "" #: src/plugins/plugin_misc_text_commands.py:328 #, python-format msgid "whois for %(jid)s" msgstr "" #: src/plugins/plugin_misc_text_commands.py:379 msgid "Invalid command name [{}]\n" msgstr "" #: src/plugins/plugin_misc_text_commands.py:397 #, python-format msgid "" "Text commands available:\n" "%s" msgstr "" #: src/plugins/plugin_misc_text_commands.py:402 #, python-brace-format msgid "" "/{name}: {short_help}\n" "{syntax}{args_help}" msgstr "" #: src/plugins/plugin_misc_text_commands.py:405 msgid " " msgstr "" #: src/plugins/plugin_misc_text_syntaxes.py:37 src/test/constants.py:44 msgid "Composition" msgstr "" #: src/plugins/plugin_misc_text_syntaxes.py:63 msgid "Management of various text syntaxes (XHTML-IM, Markdown, etc)" msgstr "" #: src/plugins/plugin_misc_text_syntaxes.py:102 msgid "Text syntaxes plugin initialization" msgstr "" #: src/plugins/plugin_misc_upload.py:38 msgid "File upload management" msgstr "" #: src/plugins/plugin_misc_upload.py:42 msgid "Please select a file to upload" msgstr "" #: src/plugins/plugin_misc_upload.py:43 msgid "File upload" msgstr "" #: src/plugins/plugin_misc_upload.py:51 msgid "plugin Upload initialization" msgstr "" #: src/plugins/plugin_misc_watched.py:36 msgid "Watch for entities presence, and send notification accordingly" msgstr "" #: src/plugins/plugin_misc_watched.py:42 #, python-brace-format msgid "Watched entity {entity} is connected" msgstr "" #: src/plugins/plugin_misc_watched.py:61 msgid "Watched initialisation" msgstr "" #: src/plugins/plugin_misc_welcome.py:33 msgid "" "Plugin which manage welcome message and things to to on first connection." msgstr "" #: src/plugins/plugin_misc_welcome.py:39 msgid "Display welcome message" msgstr "" #: src/plugins/plugin_misc_welcome.py:40 msgid "Welcome to Libervia/Salut à Toi" msgstr "" #: src/plugins/plugin_misc_welcome.py:43 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 "" #: src/plugins/plugin_misc_welcome.py:69 msgid "plugin Welcome initialization" msgstr "" #: src/plugins/plugin_misc_xmllog.py:35 msgid "Send raw XML logs to bridge" msgstr "" #: src/plugins/plugin_misc_xmllog.py:48 msgid "INTERNAL ERROR: Unmanaged XML type" msgstr "" #: src/plugins/plugin_misc_xmllog.py:69 msgid "Activate XML log" msgstr "" #: src/plugins/plugin_misc_xmllog.py:72 msgid "Plugin XML Log initialization" msgstr "" #: src/plugins/plugin_misc_xmllog.py:83 msgid "XML log activated" msgstr "" #: src/plugins/plugin_sec_otr.py:47 msgid "Implementation of OTR" msgstr "" #: src/plugins/plugin_sec_otr.py:52 msgid "OTR" msgstr "" #: src/plugins/plugin_sec_otr.py:53 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 "" #: src/plugins/plugin_sec_otr.py:54 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 "" #: src/plugins/plugin_sec_otr.py:56 msgid "Some of advanced features are disabled !" msgstr "" #: src/plugins/plugin_sec_otr.py:120 #, python-format msgid "/!\\ conversation with %(other_jid)s is now UNENCRYPTED" msgstr "" #: src/plugins/plugin_sec_otr.py:127 msgid "trusted" msgstr "" #: src/plugins/plugin_sec_otr.py:127 msgid "untrusted" msgstr "" #: src/plugins/plugin_sec_otr.py:130 #, python-brace-format msgid "{trusted} OTR conversation with {other_jid} REFRESHED" msgstr "" #: src/plugins/plugin_sec_otr.py:134 #, python-brace-format msgid "" "{trusted} encrypted OTR conversation started with {other_jid}\n" "{extra_info}" msgstr "" #: src/plugins/plugin_sec_otr.py:140 #, python-brace-format msgid "OTR conversation with {other_jid} is FINISHED" msgstr "" #: src/plugins/plugin_sec_otr.py:143 msgid "Unknown OTR state" msgstr "" #: src/plugins/plugin_sec_otr.py:178 msgid "Save is called but privkey is None !" msgstr "" #: src/plugins/plugin_sec_otr.py:230 msgid "OTR plugin initialization" msgstr "" #: src/plugins/plugin_sec_otr.py:241 msgid "Start/Refresh" msgstr "" #: src/plugins/plugin_sec_otr.py:241 msgid "Start or refresh an OTR session" msgstr "" #: src/plugins/plugin_sec_otr.py:242 msgid "End session" msgstr "" #: src/plugins/plugin_sec_otr.py:242 msgid "Finish an OTR session" msgstr "" #: src/plugins/plugin_sec_otr.py:243 msgid "Authenticate" msgstr "" #: src/plugins/plugin_sec_otr.py:243 msgid "Authenticate user/see your fingerprint" msgstr "" #: src/plugins/plugin_sec_otr.py:244 msgid "Drop private key" msgstr "" #: src/plugins/plugin_sec_otr.py:355 msgid "You have no private key yet, start an OTR conversation to have one" msgstr "" #: src/plugins/plugin_sec_otr.py:358 msgid "No private key" msgstr "" #: src/plugins/plugin_sec_otr.py:368 #, python-brace-format msgid "" "Your fingerprint is:\n" "{fingerprint}\n" "\n" "Start an OTR conversation to have your correspondent one." msgstr "" #: src/plugins/plugin_sec_otr.py:371 msgid "Fingerprint" msgstr "" #: src/plugins/plugin_sec_otr.py:380 #, python-brace-format msgid "Your correspondent {correspondent} is now TRUSTED" msgstr "" #: src/plugins/plugin_sec_otr.py:384 #, python-brace-format msgid "Your correspondent {correspondent} is now UNTRUSTED" msgstr "" #: src/plugins/plugin_sec_otr.py:395 #, python-format msgid "Authentication (%s)" msgstr "" #: src/plugins/plugin_sec_otr.py:398 #, python-brace-format msgid "" "Your own fingerprint is:\n" "{fingerprint}" msgstr "" #: src/plugins/plugin_sec_otr.py:399 #, python-brace-format msgid "" "Your correspondent fingerprint should be:\n" "{fingerprint}" msgstr "" #: src/plugins/plugin_sec_otr.py:402 msgid "Is your correspondent fingerprint the same as here ?" msgstr "" #: src/plugins/plugin_sec_otr.py:403 msgid "yes" msgstr "" #: src/plugins/plugin_sec_otr.py:403 msgid "no" msgstr "" #: src/plugins/plugin_sec_otr.py:423 msgid "You don't have a private key yet !" msgstr "" #: src/plugins/plugin_sec_otr.py:432 msgid "Your private key has been dropped" msgstr "" #: src/plugins/plugin_sec_otr.py:437 msgid "Confirm private key drop" msgstr "" #: src/plugins/plugin_sec_otr.py:454 msgid "WARNING: received unencrypted data in a supposedly encrypted context" msgstr "" #: src/plugins/plugin_sec_otr.py:531 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 "" #: src/plugins/plugin_sec_otr.py:533 msgid "Message discarded because closed encryption channel" msgstr "" #: src/plugins/plugin_syntax_wiki_dotclear.py:39 msgid "Implementation of Dotclear wiki syntax" msgstr "" #: src/plugins/plugin_syntax_wiki_dotclear.py:635 msgid "Dotclear wiki syntax plugin initialization" msgstr "" #: src/plugins/plugin_tmp_directory_subscription.py:36 msgid "Implementation of directory subscription" msgstr "" #: src/plugins/plugin_tmp_directory_subscription.py:47 msgid "Directory subscription plugin initialization" msgstr "" #: src/plugins/plugin_tmp_directory_subscription.py:49 #: src/plugins/plugin_xep_0050.py:222 src/plugins/plugin_xep_0100.py:66 msgid "Service" msgstr "" #: src/plugins/plugin_tmp_directory_subscription.py:49 msgid "Directory subscription" msgstr "" #: src/plugins/plugin_tmp_directory_subscription.py:49 msgid "User directory subscription" msgstr "" #: src/plugins/plugin_xep_0020.py:45 msgid "Implementation of Feature Negotiation" msgstr "" #: src/plugins/plugin_xep_0020.py:52 msgid "Plugin XEP_0020 initialization" msgstr "" #: src/plugins/plugin_xep_0020.py:103 msgid "More than one value choosed for {}, keeping the first one" msgstr "" #: src/plugins/plugin_xep_0033.py:64 msgid "Implementation of Extended Stanza Addressing" msgstr "" #: src/plugins/plugin_xep_0033.py:73 msgid "Extended Stanza Addressing plugin initialization" msgstr "" #: src/plugins/plugin_xep_0033.py:89 msgid "XEP-0033 is being used but the server doesn't support it!" msgstr "" #: src/plugins/plugin_xep_0033.py:92 msgid " or " msgstr "" #: src/plugins/plugin_xep_0033.py:93 #, python-format msgid "" "Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!" msgstr "" #: src/plugins/plugin_xep_0033.py:94 msgid "" "TODO: addressing has been fixed by the backend... fix it in the frontend!" msgstr "" #: src/plugins/plugin_xep_0045.py:52 msgid "Implementation of Multi-User Chat" msgstr "" #: src/plugins/plugin_xep_0045.py:78 msgid "Plugin XEP_0045 initialization" msgstr "" #: src/plugins/plugin_xep_0045.py:95 msgid "MUC" msgstr "" #: src/plugins/plugin_xep_0045.py:95 msgid "configure" msgstr "" #: src/plugins/plugin_xep_0045.py:95 msgid "Configure Multi-User Chat room" msgstr "" #: src/plugins/plugin_xep_0045.py:135 src/plugins/plugin_xep_0045.py:728 msgid "This room has not been joined" msgstr "" #: src/plugins/plugin_xep_0045.py:168 msgid "Room joining cancelled by user" msgstr "" #: src/plugins/plugin_xep_0045.py:173 msgid "Rooms in {}" msgstr "" #: src/plugins/plugin_xep_0045.py:188 msgid "room locked !" msgstr "" #: src/plugins/plugin_xep_0045.py:190 msgid "Error while configuring the room" msgstr "" #: src/plugins/plugin_xep_0045.py:206 msgid "Room {} is restricted" msgstr "" #: src/plugins/plugin_xep_0045.py:207 msgid "This room is restricted, please enter the password" msgstr "" #: src/plugins/plugin_xep_0045.py:215 #, python-brace-format msgid "Error while joining the room {room}{suffix}" msgstr "" #: src/plugins/plugin_xep_0045.py:218 msgid "Group chat error" msgstr "" #: src/plugins/plugin_xep_0045.py:307 msgid "room_jid key is not present !" msgstr "" #: src/plugins/plugin_xep_0045.py:340 src/plugins/plugin_xep_0045.py:342 msgid "Session ID doesn't exist, session has probably expired." msgstr "" #: src/plugins/plugin_xep_0045.py:341 msgid "Room configuration failed" msgstr "" #: src/plugins/plugin_xep_0045.py:347 msgid "Room configuration succeed" msgstr "" #: src/plugins/plugin_xep_0045.py:348 msgid "The new settings have been saved." msgstr "" #: src/plugins/plugin_xep_0045.py:395 msgid "No MUC service found on main server" msgstr "" #: src/plugins/plugin_xep_0045.py:424 #, python-brace-format msgid "" "Invalid room identifier: {room_id}'. Please give a room short or full " "identifier like 'room' or 'room@{muc_service}'." msgstr "" #: src/plugins/plugin_xep_0045.py:449 #, python-brace-format msgid "{profile} is already in room {room_jid}" msgstr "" #: src/plugins/plugin_xep_0045.py:451 #, python-brace-format msgid "[{profile}] is joining room {room} with nick {nick}" msgstr "" #: src/plugins/plugin_xep_0045.py:590 msgid "You must provide a member's nick to kick." msgstr "" #: src/plugins/plugin_xep_0045.py:597 msgid "You have kicked {}" msgstr "" #: src/plugins/plugin_xep_0045.py:599 src/plugins/plugin_xep_0045.py:628 msgid " for the following reason: {}" msgstr "" #: src/plugins/plugin_xep_0045.py:619 msgid "You must provide a valid JID to ban, like in '/ban contact@example.net'" msgstr "" #: src/plugins/plugin_xep_0045.py:626 msgid "You have banned {}" msgstr "" #: src/plugins/plugin_xep_0045.py:652 msgid "" "You must provide a valid JID to affiliate, like in '/affiliate " "contact@example.net member'" msgstr "" #: src/plugins/plugin_xep_0045.py:658 #, python-format msgid "You must provide a valid affiliation: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:665 #, python-format msgid "New affiliation for %(entity)s: %(affiliation)s" msgstr "" #: src/plugins/plugin_xep_0045.py:711 msgid "No known default MUC service" msgstr "" #: src/plugins/plugin_xep_0045.py:715 msgid "{} is not a valid JID!" msgstr "" #: src/plugins/plugin_xep_0045.py:733 #, python-format msgid "Nickname: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:735 #, python-format msgid "Entity: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:737 #, python-format msgid "Affiliation: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:739 #, python-format msgid "Role: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:741 #, python-format msgid "Status: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:743 #, python-format msgid "Show: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:868 #, python-brace-format msgid "user {nick} has joined room {room_id}" msgstr "" #: src/plugins/plugin_xep_0045.py:883 msgid "=> {} has joined the room" msgstr "" #: src/plugins/plugin_xep_0045.py:899 #, python-brace-format msgid "Room ({room}) left ({profile})" msgstr "" #: src/plugins/plugin_xep_0045.py:910 #, python-brace-format msgid "user {nick} left room {room_id}" msgstr "" #: src/plugins/plugin_xep_0045.py:922 msgid "<= {} has left the room" msgstr "" #: src/plugins/plugin_xep_0045.py:1034 #, python-brace-format msgid "New subject for room ({room_id}): {subject}" msgstr "" #: src/plugins/plugin_xep_0047.py:60 msgid "Implementation of In-Band Bytestreams" msgstr "" #: src/plugins/plugin_xep_0047.py:69 msgid "In-Band Bytestreams plugin initialization" msgstr "" #: src/plugins/plugin_xep_0047.py:158 msgid "IBB stream opening" msgstr "" #: src/plugins/plugin_xep_0047.py:168 #, python-format msgid "Ignoring unexpected IBB transfer: %s" msgstr "" #: src/plugins/plugin_xep_0047.py:172 msgid "sended jid inconsistency (man in the middle attack attempt ?)" msgstr "" #: src/plugins/plugin_xep_0047.py:201 msgid "IBB stream closing" msgstr "" #: src/plugins/plugin_xep_0047.py:225 msgid "Received data for an unknown session id" msgstr "" #: src/plugins/plugin_xep_0047.py:232 #, python-brace-format msgid "" "sended jid inconsistency (man in the middle attack attempt ?)\n" "initial={initial}\n" "given={given}" msgstr "" #: src/plugins/plugin_xep_0047.py:239 msgid "Sequence error" msgstr "" #: src/plugins/plugin_xep_0047.py:254 msgid "Invalid base64 data" msgstr "" #: src/plugins/plugin_xep_0048.py:44 msgid "Implementation of bookmarks" msgstr "" #: src/plugins/plugin_xep_0048.py:57 msgid "Bookmarks plugin initialization" msgstr "" #: src/plugins/plugin_xep_0048.py:61 msgid "Bookmarks" msgstr "" #: src/plugins/plugin_xep_0048.py:61 msgid "Use and manage bookmarks" msgstr "" #: src/plugins/plugin_xep_0048.py:109 msgid "Private XML storage not available" msgstr "" #: src/plugins/plugin_xep_0048.py:218 msgid "No room jid selected" msgstr "" #: src/plugins/plugin_xep_0048.py:236 msgid "Bookmarks manager" msgstr "" #: src/plugins/plugin_xep_0048.py:254 msgid "Name" msgstr "" #: src/plugins/plugin_xep_0048.py:258 msgid "Nickname" msgstr "" #: src/plugins/plugin_xep_0048.py:260 msgid "Autojoin" msgstr "" #: src/plugins/plugin_xep_0048.py:307 msgid "Bookmarks will be local only" msgstr "" #: src/plugins/plugin_xep_0048.py:308 #, python-format msgid "Type selected for \"auto\" storage: %s" msgstr "" #: src/plugins/plugin_xep_0048.py:431 msgid "Bad arguments" msgstr "" #: src/plugins/plugin_xep_0048.py:438 #, python-format msgid "All [%s] bookmarks are being removed" msgstr "" #: src/plugins/plugin_xep_0048.py:446 msgid "Bookmark added" msgstr "" #: src/plugins/plugin_xep_0049.py:37 msgid "Implementation of private XML storage" msgstr "" #: src/plugins/plugin_xep_0049.py:45 msgid "Plugin XEP-0049 initialization" msgstr "" #: src/plugins/plugin_xep_0050.py:55 msgid "Away" msgstr "" #: src/plugins/plugin_xep_0050.py:58 msgid "Left" msgstr "" #: src/plugins/plugin_xep_0050.py:68 msgid "Implementation of Ad-Hoc Commands" msgstr "" #: src/plugins/plugin_xep_0050.py:110 #, python-format msgid "The groups [%(group)s] is unknown for profile [%(profile)s])" msgstr "" #: src/plugins/plugin_xep_0050.py:214 msgid "plugin XEP-0050 initialization" msgstr "" #: src/plugins/plugin_xep_0050.py:222 msgid "Commands" msgstr "" #: src/plugins/plugin_xep_0050.py:222 msgid "Execute ad-hoc commands" msgstr "" #: src/plugins/plugin_xep_0050.py:228 msgid "Status" msgstr "" #: src/plugins/plugin_xep_0050.py:241 msgid "Please select a command" msgstr "" #: src/plugins/plugin_xep_0050.py:259 #, python-format msgid "Invalid note type [%s], using info" msgstr "" #: src/plugins/plugin_xep_0050.py:269 msgid "WARNING" msgstr "" #: src/plugins/plugin_xep_0050.py:270 msgid "ERROR" msgstr "" #: src/plugins/plugin_xep_0050.py:302 msgid "No known payload found in ad-hoc command result, aborting" msgstr "" #: src/plugins/plugin_xep_0050.py:306 msgid "No payload found" msgstr "" #: src/plugins/plugin_xep_0050.py:401 msgid "Please enter target jid" msgstr "" #: src/plugins/plugin_xep_0050.py:415 msgid "status selection" msgstr "" #: src/plugins/plugin_xep_0050.py:443 msgid "Status updated" msgstr "" #: src/plugins/plugin_xep_0054.py:73 msgid "Implementation of vcard-temp" msgstr "" #: src/plugins/plugin_xep_0054.py:83 msgid "Plugin XEP_0054 initialization" msgstr "" #: src/plugins/plugin_xep_0054.py:234 msgid "Decoding binary" msgstr "" #: src/plugins/plugin_xep_0054.py:294 msgid "VCard found" msgstr "" #: src/plugins/plugin_xep_0055.py:51 msgid "Implementation of Jabber Search" msgstr "" #: src/plugins/plugin_xep_0055.py:66 msgid "Jabber search plugin initialization" msgstr "" #: src/plugins/plugin_xep_0055.py:80 msgid "Search directory" msgstr "" #: src/plugins/plugin_xep_0055.py:80 msgid "Search user directory" msgstr "" #: src/plugins/plugin_xep_0055.py:118 msgid "Search users" msgstr "" #: src/plugins/plugin_xep_0055.py:139 msgid "Search for" msgstr "" #: src/plugins/plugin_xep_0055.py:141 msgid "Simple search" msgstr "" #: src/plugins/plugin_xep_0055.py:149 src/plugins/plugin_xep_0055.py:249 msgid "Search" msgstr "" #: src/plugins/plugin_xep_0055.py:178 msgid "Advanced search" msgstr "" #: src/plugins/plugin_xep_0055.py:196 msgid "Search on" msgstr "" #: src/plugins/plugin_xep_0055.py:198 msgid "Other service" msgstr "" #: src/plugins/plugin_xep_0055.py:205 msgid "Refresh fields" msgstr "" #: src/plugins/plugin_xep_0055.py:208 msgid "Displaying the search form for" msgstr "" #: src/plugins/plugin_xep_0055.py:281 msgid "Search results" msgstr "" #: src/plugins/plugin_xep_0055.py:286 msgid "The search gave no result" msgstr "" #: src/plugins/plugin_xep_0055.py:328 src/plugins/plugin_xep_0055.py:429 msgid "No query element found" msgstr "" #: src/plugins/plugin_xep_0055.py:333 src/plugins/plugin_xep_0055.py:434 msgid "No data form found" msgstr "" #: src/plugins/plugin_xep_0055.py:343 #, python-format msgid "Fields request failure: %s" msgstr "" #: src/plugins/plugin_xep_0055.py:415 msgid "The search could not be performed" msgstr "" #: src/plugins/plugin_xep_0055.py:444 #, python-format msgid "Search request failure: %s" msgstr "" #: src/plugins/plugin_xep_0059.py:41 msgid "Implementation of Result Set Management" msgstr "" #: src/plugins/plugin_xep_0059.py:49 msgid "Result Set Management plugin initialization" msgstr "" #: src/plugins/plugin_xep_0060.py:53 msgid "Implementation of PubSub Protocol" msgstr "" #: src/plugins/plugin_xep_0060.py:83 msgid "PubSub plugin initialization" msgstr "" #: src/plugins/plugin_xep_0065.py:95 msgid "Implementation of SOCKS5 Bytestreams" msgstr "" #: src/plugins/plugin_xep_0065.py:515 msgid "File transfer completed, closing connection" msgstr "" #: src/plugins/plugin_xep_0065.py:667 #, python-format msgid "Socks 5 client connection lost (reason: %s)" msgstr "" #: src/plugins/plugin_xep_0065.py:695 msgid "Plugin XEP_0065 initialization" msgstr "" #: src/plugins/plugin_xep_0065.py:750 msgid "Socks5 Stream server launched on port {}" msgstr "" #: src/plugins/plugin_xep_0070.py:54 msgid "Implementation of HTTP Requests via XMPP" msgstr "" #: src/plugins/plugin_xep_0070.py:64 msgid "Plugin XEP_0070 initialization" msgstr "" #: src/plugins/plugin_xep_0070.py:77 msgid "XEP-0070 Verifying HTTP Requests via XMPP (iq)" msgstr "" #: src/plugins/plugin_xep_0070.py:86 msgid "XEP-0070 Verifying HTTP Requests via XMPP (message)" msgstr "" #: src/plugins/plugin_xep_0070.py:97 msgid "Auth confirmation" msgstr "" #: src/plugins/plugin_xep_0070.py:98 msgid "{} needs to validate your identity, do you agree ?" msgstr "" #: src/plugins/plugin_xep_0070.py:99 msgid "Validation code : {}" msgstr "" #: src/plugins/plugin_xep_0070.py:100 msgid "Please check that this code is the same as on {}" msgstr "" #: src/plugins/plugin_xep_0070.py:102 msgid "Submit to authorize, cancel otherwise." msgstr "" #: src/plugins/plugin_xep_0070.py:131 msgid "XEP-0070 reply iq" msgstr "" #: src/plugins/plugin_xep_0070.py:136 msgid "XEP-0070 reply message" msgstr "" #: src/plugins/plugin_xep_0070.py:141 msgid "XEP-0070 reply error" msgstr "" #: src/plugins/plugin_xep_0071.py:51 msgid "Implementation of XHTML-IM" msgstr "" #: src/plugins/plugin_xep_0071.py:79 msgid "XHTML-IM plugin initialization" msgstr "" #: src/plugins/plugin_xep_0071.py:196 msgid "Can't have XHTML and rich content at the same time" msgstr "" #: src/plugins/plugin_xep_0077.py:40 msgid "Implementation of in-band registration" msgstr "" #: src/plugins/plugin_xep_0077.py:47 msgid "Plugin XEP_0077 initialization" msgstr "" #: src/plugins/plugin_xep_0077.py:65 msgid "Can't find data form" msgstr "" #: src/plugins/plugin_xep_0077.py:66 msgid "This gateway can't be managed by SàT, sorry :(" msgstr "" #: src/plugins/plugin_xep_0077.py:91 #, python-format msgid "registration answer: %s" msgstr "" #: src/plugins/plugin_xep_0077.py:99 msgid "Username already exists, please choose an other one" msgstr "" #: src/plugins/plugin_xep_0077.py:108 #, python-format msgid "Asking registration for [%s]" msgstr "" #: src/plugins/plugin_xep_0085.py:53 msgid "Implementation of Chat State Notifications Protocol" msgstr "" #: src/plugins/plugin_xep_0085.py:93 msgid "Enable chat state notifications" msgstr "" #: src/plugins/plugin_xep_0085.py:97 msgid "Chat State Notifications plugin initialization" msgstr "" #: src/plugins/plugin_xep_0092.py:41 msgid "Implementation of Software Version" msgstr "" #: src/plugins/plugin_xep_0092.py:48 msgid "Plugin XEP_0092 initialization" msgstr "" #: src/plugins/plugin_xep_0092.py:111 #, python-format msgid "Operating system: %s" msgstr "" #: src/plugins/plugin_xep_0092.py:115 msgid "Software version not available" msgstr "" #: src/plugins/plugin_xep_0092.py:117 msgid "Client software version request timeout" msgstr "" #: src/plugins/plugin_xep_0095.py:40 msgid "Implementation of Stream Initiation" msgstr "" #: src/plugins/plugin_xep_0095.py:54 msgid "Plugin XEP_0095 initialization" msgstr "" #: src/plugins/plugin_xep_0095.py:80 msgid "XEP-0095 Stream initiation" msgstr "" #: src/plugins/plugin_xep_0095.py:122 msgid "sending stream initiation accept answer" msgstr "" #: src/plugins/plugin_xep_0095.py:159 #, python-format msgid "Stream Session ID: %s" msgstr "" #: src/plugins/plugin_xep_0096.py:45 msgid "Implementation of SI File Transfer" msgstr "" #: src/plugins/plugin_xep_0096.py:53 msgid "Plugin XEP_0096 initialization" msgstr "" #: src/plugins/plugin_xep_0096.py:119 msgid "XEP-0096 file transfer requested" msgstr "" #: src/plugins/plugin_xep_0096.py:337 msgid "The contact {} has refused your file" msgstr "" #: src/plugins/plugin_xep_0096.py:338 msgid "File refused" msgstr "" #: src/plugins/plugin_xep_0096.py:341 msgid "Error during file transfer" msgstr "" #: src/plugins/plugin_xep_0096.py:342 #, python-brace-format msgid "" "Something went wrong during the file transfer session initialisation: " "{reason}" msgstr "" #: src/plugins/plugin_xep_0096.py:343 msgid "File transfer error" msgstr "" #: src/plugins/plugin_xep_0096.py:351 #, python-brace-format msgid "transfer {sid} successfuly finished [{profile}]" msgstr "" #: src/plugins/plugin_xep_0096.py:357 #, python-brace-format msgid "transfer {sid} failed [{profile}]: {reason}" msgstr "" #: src/plugins/plugin_xep_0100.py:36 msgid "Implementation of Gateways protocol" msgstr "" #: src/plugins/plugin_xep_0100.py:39 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 "" #: src/plugins/plugin_xep_0100.py:44 msgid "Internet Relay Chat" msgstr "" #: src/plugins/plugin_xep_0100.py:45 msgid "XMPP" msgstr "" #: src/plugins/plugin_xep_0100.py:46 msgid "Tencent QQ" msgstr "" #: src/plugins/plugin_xep_0100.py:47 msgid "SIP/SIMPLE" msgstr "" #: src/plugins/plugin_xep_0100.py:48 msgid "ICQ" msgstr "" #: src/plugins/plugin_xep_0100.py:49 msgid "Yahoo! Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:50 msgid "Gadu-Gadu" msgstr "" #: src/plugins/plugin_xep_0100.py:51 msgid "AOL Instant Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:52 msgid "Windows Live Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:59 msgid "Gateways plugin initialization" msgstr "" #: src/plugins/plugin_xep_0100.py:66 msgid "Gateways" msgstr "" #: src/plugins/plugin_xep_0100.py:66 msgid "Find gateways" msgstr "" #: src/plugins/plugin_xep_0100.py:84 #, python-format msgid "Gateways manager (%s)" msgstr "" #: src/plugins/plugin_xep_0100.py:92 #, python-format msgid "Failed (%s)" msgstr "" #: src/plugins/plugin_xep_0100.py:105 msgid "Use external XMPP server" msgstr "" #: src/plugins/plugin_xep_0100.py:107 msgid "Go !" msgstr "" #: src/plugins/plugin_xep_0100.py:114 msgid "No gateway index selected" msgstr "" #: src/plugins/plugin_xep_0100.py:128 #, python-format msgid "" "INTERNAL ERROR: identity category should always be \"gateway\" in " "_getTypeString, got \"%s\"" msgstr "" #: src/plugins/plugin_xep_0100.py:132 msgid "Unknown IM" msgstr "" #: src/plugins/plugin_xep_0100.py:136 msgid "Registration successful, doing the rest" msgstr "" #: src/plugins/plugin_xep_0100.py:159 msgid "Timeout" msgstr "" #: src/plugins/plugin_xep_0100.py:170 #, python-format msgid "Found gateway [%(jid)s]: %(identity_name)s" msgstr "" #: src/plugins/plugin_xep_0100.py:173 #, python-format msgid "Skipping [%(jid)s] which is not a gateway" msgstr "" #: src/plugins/plugin_xep_0100.py:180 msgid "No gateway found" msgstr "" #: src/plugins/plugin_xep_0100.py:185 #, python-format msgid "item found: %s" msgstr "" #: src/plugins/plugin_xep_0100.py:206 #, python-format msgid "find gateways (target = %(target)s, profile = %(profile)s)" msgstr "" #: src/plugins/plugin_xep_0115.py:48 msgid "Implementation of entity capabilities" msgstr "" #: src/plugins/plugin_xep_0115.py:56 msgid "Plugin XEP_0115 initialization" msgstr "" #: src/plugins/plugin_xep_0115.py:73 msgid "Caps optimisation enabled" msgstr "" #: src/plugins/plugin_xep_0115.py:76 msgid "Caps optimisation not available" msgstr "" #: src/plugins/plugin_xep_0115.py:143 #, python-format msgid "Received invalid capabilities tag: %s" msgstr "" #: src/plugins/plugin_xep_0115.py:153 #, python-format msgid "" "Unknown hash method for entity capabilities: [%(hash_method)s] (entity: " "%(jid)s, node: %(node)s)" msgstr "" #: src/plugins/plugin_xep_0115.py:158 #, python-format msgid "" "Computed hash differ from given hash:\n" "given: [%(given_hash)s]\n" "computed: [%(computed_hash)s]\n" "(entity: %(jid)s, node: %(node)s)" msgstr "" #: src/plugins/plugin_xep_0115.py:164 #, python-brace-format msgid "Couldn't retrieve disco info for {jid}: {error}" msgstr "" #: src/plugins/plugin_xep_0163.py:40 msgid "Implementation of Personal Eventing Protocol" msgstr "" #: src/plugins/plugin_xep_0163.py:47 msgid "PEP plugin initialization" msgstr "" #: src/plugins/plugin_xep_0163.py:114 #, python-format msgid "Trying to send personal event with an unknown profile key [%s]" msgstr "" #: src/plugins/plugin_xep_0163.py:117 msgid "Trying to send personal event for an unknown type" msgstr "" #: src/plugins/plugin_xep_0163.py:123 msgid "No item found" msgstr "" #: src/plugins/plugin_xep_0163.py:128 msgid "Can't find mood element in mood event" msgstr "" #: src/plugins/plugin_xep_0163.py:132 msgid "No mood found" msgstr "" #: src/plugins/plugin_xep_0166.py:48 #, python-brace-format msgid "{entity} want to start a jingle session with you, do you accept ?" msgstr "" #: src/plugins/plugin_xep_0166.py:57 msgid "Implementation of Jingle" msgstr "" #: src/plugins/plugin_xep_0166.py:91 msgid "plugin Jingle initialization" msgstr "" #: src/plugins/plugin_xep_0166.py:429 msgid "Confirm Jingle session" msgstr "" #: src/plugins/plugin_xep_0184.py:61 msgid "Implementation of Message Delivery Receipts" msgstr "" #: src/plugins/plugin_xep_0184.py:85 msgid "Enable message delivery receipts" msgstr "" #: src/plugins/plugin_xep_0184.py:89 msgid "Plugin XEP_0184 (message delivery receipts) initialization" msgstr "" #: src/plugins/plugin_xep_0184.py:114 msgid "[XEP-0184] Request acknowledgment for message id {}" msgstr "" #: src/plugins/plugin_xep_0184.py:147 msgid "[XEP-0184] Receive acknowledgment for message id {}" msgstr "" #: src/plugins/plugin_xep_0184.py:154 msgid "[XEP-0184] Delete waiting acknowledgment for message id {}" msgstr "" #: src/plugins/plugin_xep_0203.py:43 msgid "Implementation of Delayed Delivery" msgstr "" #: src/plugins/plugin_xep_0203.py:50 msgid "Delayed Delivery plugin initialization" msgstr "" #: src/plugins/plugin_xep_0231.py:39 msgid "Implementation of bits of binary (used for small images/files)" msgstr "" #: src/plugins/plugin_xep_0231.py:48 msgid "plugin Bits of Binary initialization" msgstr "" #: src/plugins/plugin_xep_0234.py:48 msgid "Implementation of Jingle File Transfer" msgstr "" #: src/plugins/plugin_xep_0234.py:57 msgid "plugin Jingle File Transfer initialization" msgstr "" #: src/plugins/plugin_xep_0249.py:55 msgid "Implementation of Direct MUC Invitations" msgstr "" #: src/plugins/plugin_xep_0249.py:75 msgid "Auto-join MUC on invitation" msgstr "" #: src/plugins/plugin_xep_0249.py:82 msgid "Plugin XEP_0249 initialization" msgstr "" #: src/plugins/plugin_xep_0249.py:131 #, python-format msgid "Invitation accepted for room %(room)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0249.py:144 #, python-format msgid "Invitation received for room %(room)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0249.py:146 msgid "Error while parsing invitation" msgstr "" #: src/plugins/plugin_xep_0249.py:155 msgid "Invitation silently discarded because user is already in the room." msgstr "" #: src/plugins/plugin_xep_0249.py:163 #, python-format msgid "" "An invitation from %(user)s to join the room %(room)s has been declined " "according to your personal settings." msgstr "" #: src/plugins/plugin_xep_0249.py:164 src/plugins/plugin_xep_0249.py:168 msgid "MUC invitation" msgstr "" #: src/plugins/plugin_xep_0249.py:167 #, python-format msgid "" "You have been invited by %(user)s to join the room %(room)s. Do you accept?" msgstr "" #: src/plugins/plugin_xep_0249.py:187 #, python-brace-format msgid "" "You must provide a valid JID to invite, like in '/invite contact@{host}'" msgstr "" #: src/plugins/plugin_xep_0260.py:49 msgid "Implementation of Jingle SOCKS5 Bytestreams" msgstr "" #: src/plugins/plugin_xep_0260.py:63 msgid "plugin Jingle SOCKS5 Bytestreams" msgstr "" #: src/plugins/plugin_xep_0261.py:45 msgid "Implementation of Jingle In-Band Bytestreams" msgstr "" #: src/plugins/plugin_xep_0261.py:53 msgid "plugin Jingle In-Band Bytestreams" msgstr "" #: src/plugins/plugin_xep_0277.py:57 msgid "Implementation of microblogging Protocol" msgstr "" #: src/plugins/plugin_xep_0277.py:69 msgid "Microblogging plugin initialization" msgstr "" #: src/plugins/plugin_xep_0277.py:171 msgid "Content of type XHTML must declare its namespace!" msgstr "" #: src/plugins/plugin_xep_0277.py:360 msgid "Can't have xhtml and rich content at the same time" msgstr "" #: src/plugins/plugin_xep_0277.py:636 msgid "Can't find profile's jid" msgstr "" #: src/plugins/plugin_xep_0277.py:642 #, python-format msgid "Microblog node has now access %s" msgstr "" #: src/plugins/plugin_xep_0277.py:646 msgid "Can't set microblog access" msgstr "" #: src/plugins/plugin_xep_0280.py:38 msgid "Message carbons" msgstr "" #: src/plugins/plugin_xep_0280.py:49 msgid "Implementation of Message Carbons" msgstr "" #: src/plugins/plugin_xep_0280.py:74 msgid "Plugin XEP_0280 initialization" msgstr "" #: src/plugins/plugin_xep_0280.py:99 msgid "Not activating message carbons as requested in params" msgstr "" #: src/plugins/plugin_xep_0280.py:104 msgid "server doesn't handle message carbons" msgstr "" #: src/plugins/plugin_xep_0280.py:106 msgid "message carbons available, enabling it" msgstr "" #: src/plugins/plugin_xep_0280.py:114 msgid "message carbons activated" msgstr "" #: src/plugins/plugin_xep_0297.py:44 msgid "Implementation of Stanza Forwarding" msgstr "" #: src/plugins/plugin_xep_0297.py:52 msgid "Stanza Forwarding plugin initialization" msgstr "" #: src/plugins/plugin_xep_0300.py:42 msgid "Management of cryptographic hashes" msgstr "" #: src/plugins/plugin_xep_0300.py:60 msgid "plugin Hashes initialization" msgstr "" #: src/plugins/plugin_xep_0313.py:48 msgid "Implementation of Message Archive Management" msgstr "" #: src/plugins/plugin_xep_0313.py:55 msgid "Message Archive Management plugin initialization" msgstr "" #: src/plugins/plugin_xep_0334.py:44 msgid "Implementation of Message Processing Hints" msgstr "" #: src/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 "" #: src/plugins/plugin_xep_0334.py:63 msgid "Message Processing Hints plugin initialization" msgstr "" #: src/plugins/plugin_xep_0363.py:52 msgid "Implementation of HTTP File Upload" msgstr "" #: src/plugins/plugin_xep_0363.py:91 msgid "plugin HTTP File Upload initialization" msgstr "" #: src/stdui/ui_contact_list.py:36 src/stdui/ui_contact_list.py:165 #: src/stdui/ui_contact_list.py:239 msgid "Add contact" msgstr "" #: src/stdui/ui_contact_list.py:37 src/stdui/ui_contact_list.py:183 msgid "Update contact" msgstr "" #: src/stdui/ui_contact_list.py:38 msgid "Remove contact" msgstr "" #: src/stdui/ui_contact_list.py:135 msgid "Select in which groups your contact is:" msgstr "" #: src/stdui/ui_contact_list.py:148 msgid "Add group" msgstr "" #: src/stdui/ui_contact_list.py:150 msgid "Add" msgstr "" #: src/stdui/ui_contact_list.py:166 msgid "New contact identifier (JID):" msgstr "" #: src/stdui/ui_contact_list.py:178 msgid "Nothing to update" msgstr "" #: src/stdui/ui_contact_list.py:179 src/stdui/ui_contact_list.py:197 msgid "Your contact list is empty." msgstr "" #: src/stdui/ui_contact_list.py:184 msgid "Which contact do you want to update?" msgstr "" #: src/stdui/ui_contact_list.py:196 msgid "Nothing to delete" msgstr "" #: src/stdui/ui_contact_list.py:200 msgid "Who do you want to remove from your contacts?" msgstr "" #: src/stdui/ui_contact_list.py:219 msgid "Delete contact" msgstr "" #: src/stdui/ui_contact_list.py:220 #, python-format msgid "Are you sure you want to remove %s from your contact list?" msgstr "" #: src/stdui/ui_contact_list.py:240 #, python-format msgid "Please enter a valid JID (like \"contact@%s\"):" msgstr "" #: src/stdui/ui_profile_manager.py:52 msgid "Profile password for {}" msgstr "" #: src/stdui/ui_profile_manager.py:61 src/stdui/ui_profile_manager.py:98 msgid "Connection error" msgstr "" #: src/stdui/ui_profile_manager.py:66 msgid "Internal error: {}" msgstr "" #: src/stdui/ui_profile_manager.py:99 #, python-format msgid "Can't connect to %s. Please check your connection details." msgstr "" #: src/stdui/ui_profile_manager.py:103 #, python-format msgid "XMPP password for %(profile)s%(counter)s" msgstr "" #: src/stdui/ui_profile_manager.py:105 #, python-format msgid "" "Can't connect to %s. Please check your connection details or try with " "another password." msgstr "" #: src/test/constants.py:45 msgid "Enable unibox" msgstr "" #: src/test/constants.py:46 msgid "'Wysiwyg' edition" msgstr "" #: src/test/test_plugin_misc_room_game.py:43 msgid "Dummy plugin to test room game" msgstr "" #: src/tools/config.py:51 #, python-format msgid "Testing file %s" msgstr "" #: src/tools/config.py:68 #, python-format msgid "" "Config auto-update: %(option)s set to %(value)s in the file %(config_file)s" msgstr "" #: src/tools/config.py:77 msgid "Can't read main config !" msgstr "" #: src/tools/trigger.py:63 #, python-format msgid "There is already a bound priority [%s]" msgstr "" #: src/tools/trigger.py:65 #, python-format msgid "There is already a trigger with the same priority [%s]" msgstr "" #: src/tools/xml_tools.py:60 msgid "Fixed field has neither value nor label, ignoring it" msgstr "" #: src/tools/xml_tools.py:296 msgid "INTERNAL ERROR: parameters xml not valid" msgstr "" #: src/tools/xml_tools.py:305 msgid "INTERNAL ERROR: params categories must have a name" msgstr "" #: src/tools/xml_tools.py:314 msgid "INTERNAL ERROR: params must have a name" msgstr "" #: src/tools/xml_tools.py:357 msgid "The 'options' tag is not allowed in parameter of type 'list'!" msgstr "" #: src/tools/xml_tools.py:433 msgid "TabElement must be a child of TabsContainer" msgstr "" #: src/tools/xml_tools.py:532 msgid "Can't set row index if auto_index is True" msgstr "" #: src/tools/xml_tools.py:645 msgid "either items or columns need do be filled" msgstr "" #: src/tools/xml_tools.py:658 msgid "Headers lenght doesn't correspond to columns" msgstr "" #: src/tools/xml_tools.py:704 msgid "Incorrect number of items in list" msgstr "" #: src/tools/xml_tools.py:862 msgid "Value must be an integer" msgstr "" #: src/tools/xml_tools.py:877 msgid "Value must be 0, 1, false or true" msgstr "" #: src/tools/xml_tools.py:927 msgid "empty \"options\" list" msgstr "" #: src/tools/xml_tools.py:929 src/tools/xml_tools.py:963 msgid "invalid styles" msgstr "" #: src/tools/xml_tools.py:985 msgid "DialogElement must be a direct child of TopElement" msgstr "" #: src/tools/xml_tools.py:997 msgid "MessageElement must be a direct child of DialogElement" msgstr "" #: src/tools/xml_tools.py:1009 msgid "ButtonsElement must be a direct child of DialogElement" msgstr "" #: src/tools/xml_tools.py:1020 msgid "FileElement must be a direct child of DialogElement" msgstr "" #: src/tools/xml_tools.py:1076 #, python-format msgid "Unknown panel type [%s]" msgstr "" #: src/tools/xml_tools.py:1078 msgid "form XMLUI need a submit_id" msgstr "" #: src/tools/xml_tools.py:1080 msgid "container argument must be a string" msgstr "" #: src/tools/xml_tools.py:1082 msgid "dialog_opt can only be used with dialog panels" msgstr "" #: src/tools/xml_tools.py:1124 msgid "addXXX can't be used with dialogs" msgstr "" #: src/tools/xml_tools.py:1182 msgid "Submit ID must be filled for this kind of dialog" msgstr "" #: src/tools/xml_tools.py:1209 #, python-format msgid "Unknown container type [%s]" msgstr "" #: src/tools/xml_tools.py:1229 #, python-format msgid "Invalid type [%s]" msgstr "" #: src/twisted/plugins/sat_plugin.py:58 #, python-format msgid "%s XMPP client backend" msgstr "" sat-0.6.1.1+hg20180208/i18n/fr.po0000644000175500017600000034546613243470024015542 0ustar debaclelocal_src# JP French Translation. # Copyright (C) 2009, 2010 Jérôme Poisson # This file is distributed under the same license as the jp package. # Jérôme Poisson , 2009, 2010. # Goffi , 2010. #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.0.2\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2014-02-24 19:12+0100\n" "PO-Revision-Date: 2010-03-05 19:24+1100\n" "Last-Translator: Goffi \n" "Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: frontends/src/bridge/DBus.py:62 #: src/bridge/bridge_constructor/dbus_frontend_template.py:62 #: src/bridge/bridge_constructor/generated/DBus.py:62 #, fuzzy msgid "Unknown interface" msgstr "Type d'action inconnu" #: frontends/src/constants.py:38 frontends/src/constants.py:44 #: frontends/src/wix/constants.py:39 src/plugins/plugin_xep_0050.py:52 msgid "Online" msgstr "En ligne" #: frontends/src/constants.py:39 frontends/src/constants.py:45 #: frontends/src/wix/constants.py:40 src/plugins/plugin_xep_0050.py:54 msgid "Free for chat" msgstr "Libre pour discuter" #: frontends/src/constants.py:40 frontends/src/constants.py:46 msgid "Away from keyboard" msgstr "" #: frontends/src/constants.py:41 frontends/src/constants.py:47 #: src/plugins/plugin_xep_0050.py:55 msgid "Do not disturb" msgstr "" #: frontends/src/constants.py:42 frontends/src/constants.py:48 #: frontends/src/wix/constants.py:43 src/plugins/plugin_xep_0050.py:53 msgid "Away" msgstr "Absent" #: frontends/src/jp/base.py:46 msgid "" "ProgressBar not available, please download it at http://pypi.python.org/pypi/" "progressbar" msgstr "" "ProgressBar n'est pas disponible, veuillez le télécharger à http://pypi." "python.org/pypi/progressbar" #: frontends/src/jp/base.py:47 msgid "" "Progress bar deactivated\n" "--\n" msgstr "" "Barre de progression désactivée\n" "--\n" #: frontends/src/jp/base.py:79 frontends/src/quick_frontend/quick_app.py:43 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é ?" #: frontends/src/jp/base.py:82 frontends/src/quick_frontend/quick_app.py:46 #, fuzzy msgid "Can't init bridge" msgstr "Construction du jeu de Tarot" #: frontends/src/jp/base.py:90 msgid "Available commands" msgstr "" #: frontends/src/jp/base.py:112 #, python-format msgid "Use PROFILE profile key (default: %(default)s)" msgstr "" #: frontends/src/jp/base.py:113 msgid "Connect the profile before doing anything else" msgstr "" #: frontends/src/jp/base.py:117 msgid "Show progress bar" msgstr "Affiche la barre de progression" #: frontends/src/jp/base.py:145 #, python-format msgid "Invalid module %s" msgstr "" #: frontends/src/jp/base.py:161 msgid "User interruption: good bye" msgstr "Interrompu par l'utilisateur: au revoir" #: frontends/src/jp/base.py:203 #, python-format msgid "%s is not a valid JID !" msgstr "%s n'est pas un JID valide !" #: frontends/src/jp/base.py:226 #, fuzzy msgid "Can't connect profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/base.py:232 #, fuzzy, python-format msgid "The profile [%s] doesn't exist" msgstr "Le fichier [%s] n'existe pas !" #: frontends/src/jp/base.py:241 #, fuzzy, python-format msgid "" "Profile [%(profile)s] 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" #: frontends/src/jp/base.py:269 msgid "Progress: " msgstr "Progression: " #: frontends/src/jp/cmd_adhoc.py:34 #, fuzzy msgid "Remote control a software" msgstr "Supp&rimer un contact" #: frontends/src/jp/cmd_adhoc.py:37 msgid "Software name" msgstr "" #: frontends/src/jp/cmd_adhoc.py:38 msgid "Jids allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:39 msgid "Groups allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:40 msgid "Groups that are *NOT* allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:41 msgid "Jids that are *NOT* allowed to use the command" msgstr "" #: frontends/src/jp/cmd_adhoc.py:42 msgid "Loop on the commands" msgstr "" #: frontends/src/jp/cmd_adhoc.py:56 #, fuzzy, python-format msgid "Bus name found: [%s]" msgstr "Fonctionnalité trouvée: %s" #: frontends/src/jp/cmd_adhoc.py:59 #, python-format msgid "Command found: (path:%(path)s, iface: %(iface)s) [%(command)s]" msgstr "" #: frontends/src/jp/cmd_adhoc.py:68 msgid "Ad-hoc commands" msgstr "" #: frontends/src/jp/cmd_file.py:33 #, fuzzy msgid "Send a file to a contact" msgstr "Attend qu'un fichier soit envoyé par un contact" #: frontends/src/jp/cmd_file.py:36 msgid "A list of file" msgstr "" #: frontends/src/jp/cmd_file.py:37 frontends/src/jp/cmd_message.py:36 #: frontends/src/jp/cmd_pipe.py:37 msgid "The destination jid" msgstr "" #: frontends/src/jp/cmd_file.py:38 msgid "Make a bzip2 tarball" msgstr "Fait un fichier compressé bzip2" #: frontends/src/jp/cmd_file.py:50 #, fuzzy, python-format msgid "file [%s] doesn't exist !" msgstr "Le fichier [%s] n'existe pas !" #: frontends/src/jp/cmd_file.py:53 #, python-format msgid "[%s] is a dir ! Please send files inside or use compression" msgstr "" "[%s] est un répertoire ! Veuillez envoyer les fichiers qu'il contient ou " "utiliser la compression." #: frontends/src/jp/cmd_file.py:61 #, fuzzy, python-format msgid "tmp file_ (%s) already exists ! Please remove it" msgstr "le fichier temporaire (%s) existe déjà ! Veuillez le supprimer" #: frontends/src/jp/cmd_file.py:63 msgid "bz2 is an experimental option at an early dev stage, use with caution" msgstr "" "bz2 est une option expérimentale à un stade de développement peu avancé, " "utilisez-là avec prudence" #: frontends/src/jp/cmd_file.py:65 msgid "Starting compression, please wait..." msgstr "Lancement de la compression, veuillez patienter..." #: frontends/src/jp/cmd_file.py:69 #, python-format msgid "Adding %s" msgstr "Ajout de %s" #: frontends/src/jp/cmd_file.py:72 #, fuzzy msgid "Done !" msgstr "N° de Tél:" #: frontends/src/jp/cmd_file.py:85 msgid "Wait for a file to be sent by a contact" msgstr "Attend qu'un fichier soit envoyé par un contact" #: frontends/src/jp/cmd_file.py:92 frontends/src/jp/cmd_pipe.py:68 msgid "Jids accepted (none means \"accept everything\")" msgstr "" #: frontends/src/jp/cmd_file.py:93 msgid "Accept multiple files (you'll have to stop manually)" msgstr "" "Accepte plusieurs fichiers (vous devrez arrêter le programme à la main)" #: frontends/src/jp/cmd_file.py:94 msgid "Force overwritting of existing files" msgstr "Force le remplacement des fichiers existants" #: frontends/src/jp/cmd_file.py:103 #, python-format msgid "Accepted file [%(filename)s] from %(sender)s" msgstr "Le fichier [%(filename)s] de %(sender)s a été accepté" #: frontends/src/jp/cmd_file.py:107 #, python-format msgid "" "Refused file [%(filename)s] from %(sender)s: a file with the same name " "already exist" msgstr "" "Le fichier [%(filename)s] de %(sender)s a été refusé: un fichier avec le " "même nom existe déjà" #: frontends/src/jp/cmd_file.py:125 msgid "File sending/receiving" msgstr "" #: frontends/src/jp/cmd_message.py:31 #, fuzzy msgid "Send a message to a contact" msgstr "Attend qu'un fichier soit envoyé par un contact" #: frontends/src/jp/cmd_message.py:34 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." #: frontends/src/jp/cmd_message.py:35 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 ;))" #: frontends/src/jp/cmd_pipe.py:34 msgid "Pipe a stream out" msgstr "" #: frontends/src/jp/cmd_pipe.py:61 msgid "Wait for the reception of a pipe stream" msgstr "" #: frontends/src/jp/cmd_pipe.py:87 msgid "Stream piping through XMPP" msgstr "" #: frontends/src/jp/cmd_profile.py:30 #, fuzzy msgid "The name of the profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/cmd_profile.py:35 #, fuzzy msgid "Delete a profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/cmd_profile.py:50 #, fuzzy msgid "Get information about a profile" msgstr "Demande de contacts pour un profile inexistant" #: frontends/src/jp/cmd_profile.py:76 #, fuzzy msgid "List profiles" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/cmd_profile.py:89 #, fuzzy msgid "Create a new profile" msgstr "Veuillez entrer le nom du nouveau profile" #: frontends/src/jp/cmd_profile.py:92 #, fuzzy msgid "the name of the profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/cmd_profile.py:93 #, fuzzy msgid "the jid of the profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/jp/cmd_profile.py:94 msgid "the password of the profile" msgstr "" #: frontends/src/jp/cmd_profile.py:115 #, fuzzy msgid "Profile commands" msgstr "Mauvais nom de profile" #: frontends/src/primitivus/card_game.py:266 #: frontends/src/wix/card_game.py:103 msgid "Please choose your contrat" msgstr "Veuillez choisir votre contrat" #: frontends/src/primitivus/card_game.py:288 #: frontends/src/wix/card_game.py:114 src/plugins/plugin_misc_tarot.py:239 msgid "Draw game" msgstr "" #: frontends/src/primitivus/card_game.py:290 #: frontends/src/wix/card_game.py:116 msgid "You win \\o/" msgstr "Victoire \\o/" #: frontends/src/primitivus/card_game.py:290 #: frontends/src/wix/card_game.py:116 msgid "You loose :(" msgstr "Vous perdez :(" #: frontends/src/primitivus/card_game.py:302 #: frontends/src/wix/card_game.py:134 msgid "Cards played are invalid !" msgstr "Les cartes jouées sont invalides !" #: frontends/src/primitivus/card_game.py:335 #: frontends/src/wix/card_game.py:242 msgid "Do you put these cards in chien ?" msgstr "Voulez-vous placer ces cartes au chien ?" #: frontends/src/primitivus/chat.py:134 msgid "Game" msgstr "Jeu" #: frontends/src/primitivus/chat.py:135 msgid "MUC" msgstr "" #: frontends/src/primitivus/chat.py:137 #, fuzzy msgid "Configure room" msgstr " Configurer l'application" #: frontends/src/primitivus/chat.py:139 #, fuzzy msgid "Action" msgstr "Connexion..." #: frontends/src/primitivus/chat.py:139 #, fuzzy msgid "Send file" msgstr "Envoi un fichier" #: frontends/src/primitivus/chat.py:318 #, python-format msgid "Primitivus: %s is talking to you" msgstr "" #: frontends/src/primitivus/chat.py:320 #, 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)" #: frontends/src/primitivus/chat.py:337 frontends/src/wix/chat.py:273 #, fuzzy msgid "Can't start game" msgstr "Construction du jeu de Tarot" #: frontends/src/primitivus/chat.py:337 frontends/src/wix/chat.py:273 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" #: frontends/src/primitivus/chat.py:345 #: frontends/src/primitivus/primitivus:458 #: frontends/src/primitivus/primitivus:460 #: frontends/src/primitivus/primitivus:523 #: frontends/src/primitivus/primitivus:549 #: frontends/src/primitivus/primitivus:559 #: frontends/src/primitivus/primitivus:578 #: frontends/src/primitivus/primitivus:597 frontends/src/wix/card_game.py:134 #: frontends/src/wix/main_window.py:248 frontends/src/wix/main_window.py:255 #: frontends/src/wix/main_window.py:323 frontends/src/wix/main_window.py:410 #: frontends/src/wix/main_window.py:458 frontends/src/wix/main_window.py:481 msgid "Error" msgstr "Erreur" #: frontends/src/primitivus/contact_list.py:44 #, fuzzy msgid "Contacts" msgstr "&Contacts" #: frontends/src/primitivus/primitivus:108 #, fuzzy, python-format msgid "Error while sending message (%s)" msgstr "Erreur en tentant de rejoindre le salon" #: frontends/src/primitivus/primitivus:273 msgid "Pleeeeasse, I can't even breathe !" msgstr "Pitiééééééééé, je ne peux même pas respirer !" #: frontends/src/primitivus/primitivus:285 frontends/src/wix/profile.py:83 msgid "General" msgstr "Général" #: frontends/src/primitivus/primitivus:286 #: frontends/src/primitivus/profile_manager.py:49 #: frontends/src/wix/profile_manager.py:69 #, fuzzy msgid "Connect" msgstr "Connexion..." #: frontends/src/primitivus/primitivus:287 src/plugins/plugin_xep_0050.py:57 #, fuzzy msgid "Disconnect" msgstr "Déconnexion..." #: frontends/src/primitivus/primitivus:288 #, fuzzy msgid "Parameters" msgstr "&Paramètres" #: frontends/src/primitivus/primitivus:289 #: frontends/src/primitivus/primitivus:603 msgid "About" msgstr "À propos" #: frontends/src/primitivus/primitivus:290 #, fuzzy msgid "Exit" msgstr "Quitter" #: frontends/src/primitivus/primitivus:291 #, fuzzy msgid "Contact" msgstr "&Contacts" #: frontends/src/primitivus/primitivus:292 #, fuzzy msgid "Add contact" msgstr "&Ajouter un contact" #: frontends/src/primitivus/primitivus:293 #, fuzzy msgid "Remove contact" msgstr "Supp&rimer un contact" #: frontends/src/primitivus/primitivus:294 src/plugins/plugin_xep_0055.py:56 #, fuzzy msgid "Communication" msgstr "Connexion..." #: frontends/src/primitivus/primitivus:295 msgid "Join room" msgstr "Rejoindre un salon" #: frontends/src/primitivus/primitivus:307 #, fuzzy msgid "Main menu" msgstr "Construction des menus" #: frontends/src/primitivus/primitivus:354 #: frontends/src/primitivus/primitivus:382 #, fuzzy msgid "Chat menu" msgstr "Construction des menus" #: frontends/src/primitivus/primitivus:420 #: frontends/src/wix/main_window.py:203 #, fuzzy, python-format msgid "unmanaged dialog type: %s" msgstr "type d'erreur inconnu: %s" #: frontends/src/primitivus/primitivus:435 msgid "INTERNAL ERROR: Unexpected class for main widget's footer" msgstr "" "ERREUR INTERNE: Classe inattendue pour le pied de page du widget principal" #: frontends/src/primitivus/primitivus:458 #: frontends/src/wix/main_window.py:247 #, fuzzy msgid "Unmanaged action result" msgstr "Tab inconnu" #: frontends/src/primitivus/primitivus:477 #: frontends/src/wix/main_window.py:277 msgid "Where do you want to save the file ?" msgstr "Où voulez-vous sauvegarder le fichier ?" #: frontends/src/primitivus/primitivus:489 #: frontends/src/wix/main_window.py:271 #, python-format msgid "" "The contact %(jid)s wants to send you the file %(filename)s\n" "Do you accept ?" msgstr "" "Le contact %(jid)s veut vous envoyer le fichier %(filename)s\n" "Êtes vous d'accord ?" #: frontends/src/primitivus/primitivus:502 #: frontends/src/wix/main_window.py:308 msgid "unknown id, ignoring" msgstr "id inconnue, on l'ignore" #: frontends/src/primitivus/primitivus:508 #: frontends/src/wix/main_window.py:244 frontends/src/wix/main_window.py:330 msgid "XML user interface received" msgstr "Interface utilisateur XML reçue" #: frontends/src/primitivus/primitivus:511 #: frontends/src/wix/main_window.py:333 msgid "Form" msgstr "Formulaire" #: frontends/src/primitivus/primitivus:513 #: frontends/src/wix/main_window.py:335 #, fuzzy msgid "Registration" msgstr "Échec de l'inscription" #: frontends/src/primitivus/primitivus:537 #: frontends/src/wix/main_window.py:352 #, fuzzy, python-format msgid "FIXME FIXME FIXME: type [%s] not implemented" msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" #: frontends/src/primitivus/primitivus:547 #: frontends/src/primitivus/primitivus:557 #: frontends/src/wix/main_window.py:448 frontends/src/wix/main_window.py:508 #, fuzzy, python-format msgid "'%s' is an invalid JID !" msgstr "%s n'est pas un JID valide !" #: frontends/src/primitivus/primitivus:563 #: frontends/src/wix/main_window.py:471 #, python-format msgid "Unsubscribing %s presence" msgstr "Désinscription à la présence de %s" #: frontends/src/primitivus/primitivus:578 #, fuzzy msgid "Can't get parameters" msgstr "Impossible de charger les paramètres !" #: frontends/src/primitivus/primitivus:587 #: frontends/src/wix/main_window.py:502 msgid "Entering a MUC room" msgstr "Entrée dans le salon MUC" #: frontends/src/primitivus/primitivus:587 #: frontends/src/wix/main_window.py:500 #, fuzzy msgid "Please enter MUC's JID" msgstr "Veuillez entrer le JID de votre nouveau contact" #: frontends/src/primitivus/primitivus:591 #: frontends/src/wix/main_window.py:441 msgid "Adding a contact" msgstr "Ajout d'un contact" #: frontends/src/primitivus/primitivus:591 #: frontends/src/wix/main_window.py:440 msgid "Please enter new contact JID" msgstr "Veuillez entrer le JID de votre nouveau contact" #: frontends/src/primitivus/primitivus:597 #, fuzzy msgid "You have not selected any contact to delete !" msgstr "Vous n'avez sélectionné aucun contact !" #: frontends/src/primitivus/primitivus:599 #, fuzzy, python-format msgid "Are you sure you want to delete the contact [%s] ?" msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" #: frontends/src/primitivus/profile_manager.py:35 #, fuzzy msgid "Login:" msgstr "Identifiant" #: frontends/src/primitivus/profile_manager.py:36 #: frontends/src/wix/profile_manager.py:66 msgid "Password:" msgstr "Mot de passe:" #: frontends/src/primitivus/profile_manager.py:41 #: frontends/src/wix/profile_manager.py:51 msgid "New" msgstr "Nouveau" #: frontends/src/primitivus/profile_manager.py:42 #: frontends/src/wix/profile_manager.py:52 msgid "Delete" msgstr "Suppression" #: frontends/src/primitivus/profile_manager.py:54 #, fuzzy msgid "Profile Manager" msgstr "Mauvais nom de profile" #: frontends/src/primitivus/profile_manager.py:82 #, fuzzy msgid "A profile with this name already exists" msgstr "Ce nom de profile existe déjà" #: frontends/src/primitivus/profile_manager.py:84 msgid "Profile creation cancelled by backend" msgstr "" #: frontends/src/primitivus/profile_manager.py:86 msgid "Database error" msgstr "" #: frontends/src/primitivus/profile_manager.py:88 #, fuzzy, python-format msgid "Unknown reason (%s)" msgstr "Type d'action inconnu" #: frontends/src/primitivus/profile_manager.py:89 msgid "Can't create profile" msgstr "" #: frontends/src/primitivus/profile_manager.py:100 #: frontends/src/wix/profile_manager.py:95 #, fuzzy msgid "New profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/primitivus/profile_manager.py:100 #, fuzzy msgid "Please enter a new profile name" msgstr "Veuillez entrer le nom du nouveau profile" #: frontends/src/primitivus/profile_manager.py:104 #, fuzzy, python-format msgid "Are you sure you want to delete the profile %s ?" msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" #: frontends/src/primitivus/profile_manager.py:123 #: frontends/src/wix/profile_manager.py:131 msgid "No profile selected" msgstr "Aucun profile sélectionné" #: frontends/src/primitivus/profile_manager.py:123 #, fuzzy msgid "You need to create and select a profile before connecting" msgstr "" "Vous devez sélectionner un profile ou en créer un nouveau avant de vous " "connecter." #: frontends/src/primitivus/profile_manager.py:126 #: frontends/src/wix/profile_manager.py:100 #: frontends/src/wix/profile_manager.py:134 msgid "Bad profile name" msgstr "Mauvais nom de profile" #: frontends/src/primitivus/profile_manager.py:126 #: frontends/src/wix/profile_manager.py:100 #: frontends/src/wix/profile_manager.py:134 msgid "A profile name can't start with a @" msgstr "Un nom de profile ne peut pas commencer avec un @" #: frontends/src/primitivus/profile_manager.py:152 #: frontends/src/quick_frontend/quick_app.py:119 #: frontends/src/wix/profile_manager.py:158 #, fuzzy msgid "Can't get profile parameter" msgstr "Mauvais nom de profile" #: frontends/src/primitivus/progress.py:33 msgid "Clear progress list" msgstr "Effacer la liste" #: frontends/src/primitivus/status.py:46 msgid "Set your presence" msgstr "" #: frontends/src/primitivus/status.py:50 msgid "Set your status" msgstr "" #: frontends/src/primitivus/status.py:50 msgid "New status" msgstr "" #: frontends/src/primitivus/xmlui.py:76 #, fuzzy msgid "Unknown div_char" msgstr "Type d'action inconnu" #: frontends/src/primitivus/xmlui.py:225 frontends/src/wix/xmlui.py:255 msgid "Submit" msgstr "Envoyer" #: frontends/src/primitivus/xmlui.py:227 frontends/src/primitivus/xmlui.py:235 msgid "Cancel" msgstr "Annuler" #: frontends/src/primitivus/xmlui.py:234 msgid "Save" msgstr "Sauvegarder" #: frontends/src/quick_frontend/quick_app.py:98 #, fuzzy, python-format msgid "Trying to plug an unknown profile (%s)" msgstr "Tentative d'appel d'un profile inconnue" #: frontends/src/quick_frontend/quick_app.py:104 #, 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" " " #: frontends/src/quick_frontend/quick_app.py:111 msgid "Select the profile to use" msgstr "Veuillez sélectionner le profile à utiliser" #: frontends/src/quick_frontend/quick_app.py:124 msgid "There is already one profile plugged (we are in single profile mode) !" msgstr "Il y a déjà un profile utilisé (nous comme en mode profile unique) !" #: frontends/src/quick_frontend/quick_app.py:128 #, fuzzy msgid "The profile asked doesn't exist" msgstr "Le fichier [%s] n'existe pas !" #: frontends/src/quick_frontend/quick_app.py:131 #, fuzzy msgid "The profile is already plugged" msgstr "Ce nom de profile existe déjà" #: frontends/src/quick_frontend/quick_app.py:153 msgid "Error during autoconnection" msgstr "" #: frontends/src/quick_frontend/quick_app.py:207 msgid "This profile is not plugged" msgstr "Ce profile n'est pas utilisé" #: frontends/src/quick_frontend/quick_app.py:218 #, fuzzy msgid "Connected" msgstr "Connexion..." #: frontends/src/quick_frontend/quick_app.py:225 #, fuzzy msgid "Disconnected" msgstr "Déconnexion..." #: frontends/src/quick_frontend/quick_app.py:233 #, fuzzy msgid "Connection Error" msgstr "Connexion..." #: frontends/src/quick_frontend/quick_app.py:236 msgid "Can't connect to account, please check your password" msgstr "" #: frontends/src/quick_frontend/quick_app.py:236 #, fuzzy msgid "Connection error" msgstr "Connexion..." #: frontends/src/quick_frontend/quick_app.py:238 #, fuzzy, python-format msgid "FIXME: error_type %s not implemented" msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" #: frontends/src/quick_frontend/quick_app.py:290 msgid "sendMessage Error" msgstr "" #: frontends/src/quick_frontend/quick_app.py:306 #, fuzzy, python-format msgid "" "presence update for %(jid)s (show=%(show)s, priority=%(priority)s, statuses=" "%(statuses)s) [profile:%(profile)s]" msgstr "" "Mise à jour des information de présence pour [%(entity)s] (available, show=" "%(show)s statuses=%(statuses)s priority=%(priority)d)" #: frontends/src/quick_frontend/quick_app.py:323 #, fuzzy, python-format msgid "Watched jid [%s] is connected !" msgstr "Vous êtes déjà connecté !" #: frontends/src/quick_frontend/quick_app.py:336 #, fuzzy, python-format msgid "Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" msgstr "" "%(profile)s a rejoint le salon [%(room_name)s], utilisateurs présents:" "%(users)s" #: frontends/src/quick_frontend/quick_app.py:347 #, fuzzy, python-format msgid "Room [%(room_jid)s] left by %(profile)s" msgstr "contrat [%(contrat)s] choisi par %(profile)s" #: frontends/src/quick_frontend/quick_app.py:357 #, fuzzy, python-format msgid "user [%(user_nick)s] joined room [%(room_jid)s]" msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" #: frontends/src/quick_frontend/quick_app.py:365 #, fuzzy, python-format msgid "user [%(user_nick)s] left room [%(room_jid)s]" msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" #: frontends/src/quick_frontend/quick_app.py:373 #, fuzzy, python-format msgid "" "user [%(old_nick)s] is now known as [%(new_nick)s] in room [%(room_jid)s]" msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" #: frontends/src/quick_frontend/quick_app.py:381 #, fuzzy, python-format msgid "new subject for room [%(room_jid)s]: %(subject)s" msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" #: frontends/src/quick_frontend/quick_app.py:386 #, fuzzy msgid "Tarot Game Started \\o/" msgstr "Un jeu de Tarot est déjà lancé dans le salon %s" #: frontends/src/quick_frontend/quick_app.py:389 #, python-format msgid "" "new Tarot game started by [%(referee)s] in room [%(room_jid)s] with " "%(players)s" msgstr "" "nouveau jeu de Tarot lancé par [%(referee)s] dans le salon [%(room_jid)s] " "avec %(players)s" #: frontends/src/quick_frontend/quick_app.py:394 #, fuzzy msgid "New Tarot Game" msgstr "nouveau jeu de Tarot" #: frontends/src/quick_frontend/quick_app.py:402 #, fuzzy msgid "Tarot: need to select a contrat" msgstr "Vous essayez de ré-attribuer une constante" #: frontends/src/quick_frontend/quick_app.py:409 #, fuzzy msgid "Show cards" msgstr "Affichage du chat" #: frontends/src/quick_frontend/quick_app.py:416 msgid "My turn to play" msgstr "C'est à moi de jouer" #: frontends/src/quick_frontend/quick_app.py:424 #, fuzzy msgid "Tarot: score received" msgstr "tarot: chien reçu" #: frontends/src/quick_frontend/quick_app.py:431 #, fuzzy, python-format msgid "Card(s) played (%(player)s): %(cards)s" msgstr "Cartes jouées par %(profile)s: [%(cards)s]" #: frontends/src/quick_frontend/quick_app.py:438 #, python-format msgid "Cards played are not valid: %s" msgstr "Les cartes jouées sont invalides: %s" #: frontends/src/quick_frontend/quick_app.py:445 #, fuzzy msgid "Quiz Game Started \\o/" msgstr "Un jeu de Tarot est déjà lancé dans le salon %s" #: frontends/src/quick_frontend/quick_app.py:448 #, fuzzy, python-format msgid "" "new Quiz game started by [%(referee)s] in room [%(room_jid)s] with " "%(players)s" msgstr "" "nouveau jeu de Tarot lancé par [%(referee)s] dans le salon [%(room_jid)s] " "avec %(players)s" #: frontends/src/quick_frontend/quick_app.py:453 #, fuzzy msgid "New Quiz Game" msgstr "nouveau jeu de Tarot" #: frontends/src/quick_frontend/quick_app.py:461 #, python-format msgid "Quiz: new question: %s" msgstr "" #: frontends/src/quick_frontend/quick_app.py:514 #, python-format msgid "The contact %s has accepted your subscription" msgstr "Le contact %s a accepté votre inscription" #: frontends/src/quick_frontend/quick_app.py:514 #: frontends/src/quick_frontend/quick_app.py:520 #, fuzzy msgid "Subscription confirmation" msgstr "désinscription confirmée pour [%s]" #: frontends/src/quick_frontend/quick_app.py:517 #, python-format msgid "The contact %s has refused your subscription" msgstr "Le contact %s a refusé votre inscription" #: frontends/src/quick_frontend/quick_app.py:517 #, fuzzy msgid "Subscription refusal" msgstr "demande d'inscription pour [%s]" #: frontends/src/quick_frontend/quick_app.py:520 #, python-format msgid "" "The contact %s wants to subscribe to your presence.\n" "Do you accept ?" msgstr "" "Le contact %s veut s'inscrire à vos informations de présence\n" "Acceptez vous ?" #: frontends/src/quick_frontend/quick_app.py:531 #, python-format msgid "param update: [%(namespace)s] %(name)s = %(value)s" msgstr "Le paramètre [%(namespace)s] %(name)s vaut désormais %(value)s" #: frontends/src/quick_frontend/quick_app.py:533 #, python-format msgid "Changing JID to %s" msgstr "Changement du JID pour %s" #: frontends/src/quick_frontend/quick_chat.py:47 #, python-format msgid "Adding users %s to room" msgstr "Ajout de l'utilisateur %s dans le salon" #: frontends/src/quick_frontend/quick_chat.py:49 msgid "[INTERNAL] trying to set presents nicks for a non group chat window" msgstr "" "[INTERNAL] tentative d'indiquer les utilisateurs présents pour une fenêtre " "de chat qui n'est pas un chat de groupe" #: frontends/src/quick_frontend/quick_chat.py:55 frontends/src/wix/chat.py:146 #, python-format msgid "Replacing user %s" msgstr "Remplacement de l'utilisateur %s" #: frontends/src/quick_frontend/quick_chat.py:57 frontends/src/wix/chat.py:148 msgid "[INTERNAL] trying to replace user for a non group chat window" msgstr "" "[INTERNAL] tentative de remplacer un utilisateur pour une fenêtre de chat " "qui n'est pas un chat de groupe" #: frontends/src/quick_frontend/quick_chat.py:66 #, fuzzy, python-format msgid "Removing user %s" msgstr "suppression de %s" #: frontends/src/quick_frontend/quick_chat.py:68 msgid "[INTERNAL] trying to remove user for a non group chat window" msgstr "" "[INTERNAL] tentative de supprimer un utilisateur pour une fenêtre de chat " "qui n'est pas un chat de groupe" #: frontends/src/quick_frontend/quick_chat.py:83 #, python-format msgid "Changing nick of user %(old_nick)s to %(new_nick)s" msgstr "" #: frontends/src/quick_frontend/quick_chat.py:85 #, fuzzy msgid "[INTERNAL] trying to change user nick for a non group chat window" msgstr "" "[INTERNAL] tentative de remplacer un utilisateur pour une fenêtre de chat " "qui n'est pas un chat de groupe" #: frontends/src/quick_frontend/quick_chat.py:93 #, fuzzy, python-format msgid "Setting subject to %s" msgstr "Envoi du message jabber à %s" #: frontends/src/quick_frontend/quick_chat.py:95 msgid "[INTERNAL] trying to set subject for a non group chat window" msgstr "" "[INTERNAL] tentative de changer le sujet pour une fenêtre de chat qui n'est " "pas un chat de groupe" #: frontends/src/quick_frontend/quick_chat.py:100 msgid "now we print history" msgstr "Maintenant on affiche l'historique" #: frontends/src/quick_frontend/quick_chat.py:110 #, fuzzy msgid "Can't get history" msgstr "Impossible de charger l'historique !" #: frontends/src/quick_frontend/quick_chat.py:149 msgid "startGame is not implemented in this frontend" msgstr "startGame n'est pas implémenté dans ce frontend" #: frontends/src/quick_frontend/quick_chat.py:154 msgid "getGame is not implemented in this frontend" msgstr "getGame n'est pas implémenté dans ce frontend" #: frontends/src/quick_frontend/quick_contact_list.py:28 msgid "Contact List init" msgstr "Initialisation de la liste de contacts" #: frontends/src/quick_frontend/quick_contact_management.py:71 #, fuzzy msgid "Trying to get attribute for an unknown contact" msgstr "Tentative d'assigner un paramètre à un profile inconnu" #: frontends/src/quick_frontend/quick_contact_management.py:87 #, fuzzy msgid "INTERNAL ERROR: Key error" msgstr "ERREUR INTERNE: paramètres xml non valides" #: frontends/src/quick_frontend/quick_contact_management.py:99 #, fuzzy, python-format msgid "Trying to update an unknown contact: %s" msgstr "Tentative d'accès à un profile inconnu" #: frontends/src/tools/xmlui.py:191 msgid "XMLUI can have only one main container" msgstr "" #: frontends/src/tools/xmlui.py:240 #, fuzzy, python-format msgid "Unknown container [%s], using default one" msgstr "Disposition inconnue, utilisation de celle par defaut" #: frontends/src/tools/xmlui.py:249 msgid "Internal Error, container has not _xmluiAppend method" msgstr "" #: frontends/src/tools/xmlui.py:315 #, fuzzy, python-format msgid "FIXME FIXME FIXME: widget type [%s] is not implemented" msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" #: frontends/src/tools/xmlui.py:316 #, fuzzy, python-format msgid "FIXME FIXME FIXME: type [%s] is not implemented" msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" #: frontends/src/tools/xmlui.py:324 #, python-format msgid "No change listener on [%s]" msgstr "" #: frontends/src/tools/xmlui.py:330 #, fuzzy, python-format msgid "Unknown tag [%s]" msgstr "Type d'action inconnu" #: frontends/src/tools/xmlui.py:395 #, fuzzy msgid "No callback_id found" msgstr "Aucun transport trouvé" #: frontends/src/tools/xmlui.py:436 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" #: frontends/src/tools/xmlui.py:441 msgid "Cancelling form" msgstr "Annulation du formulaire" #: frontends/src/wix/card_game.py:95 #, fuzzy msgid "Contrat choosed" msgstr "Contact choisi: %s" #: frontends/src/wix/card_game.py:242 msgid "Écart" msgstr "Écart" #: frontends/src/wix/chat.py:114 #, fuzzy msgid "configure chat window for Tarot game" msgstr "Construction du jeu de Tarot" #: frontends/src/wix/chat.py:121 #, fuzzy msgid "configure chat window for Quiz game" msgstr "Construction du jeu de Tarot" #: frontends/src/wix/chat.py:175 msgid "&SendFile\tCTRL-s" msgstr "Envoi de fichier\tCTRL-s" #: frontends/src/wix/chat.py:175 #, fuzzy msgid " Send a file to contact" msgstr "Attend qu'un fichier soit envoyé par un contact" #: frontends/src/wix/chat.py:176 msgid "&Action" msgstr "&Action" #: frontends/src/wix/chat.py:187 #, fuzzy msgid "Start &Tarot game\tCTRL-t" msgstr "Construction du jeu de Tarot" #: frontends/src/wix/chat.py:187 #, fuzzy msgid " Start a Tarot card game" msgstr "Implementation de vcard-temp" #: frontends/src/wix/chat.py:188 msgid "&Games" msgstr "Jeux" #: frontends/src/wix/chat.py:256 msgid "Send File" msgstr "Envoi un fichier" #: frontends/src/wix/chat.py:257 msgid "Choose a file to send" msgstr "Veuillez choisir le fichier à envoyer" #: frontends/src/wix/chat.py:259 #, fuzzy, python-format msgid "filename: %s" msgstr "fichier enregistré dans %s" #: frontends/src/wix/chat.py:267 frontends/src/wix/main_window.py:281 msgid "File Transfer" msgstr "Transfert de fichier" #: frontends/src/wix/chat.py:267 frontends/src/wix/main_window.py:281 #, fuzzy, python-format msgid "Copying %s" msgstr "Ajout de %s" #: frontends/src/wix/chat.py:270 #, fuzzy msgid "Starting Tarot game" msgstr "Construction du jeu de Tarot" #: frontends/src/wix/chat.py:271 msgid "FIXME: temporary menu, must be changed" msgstr "CORRIGEZ-MOI: menu temporaire, doit être remplacé" #: frontends/src/wix/constants.py:36 #, fuzzy msgid "offline" msgstr "En ligne" #: frontends/src/wix/constants.py:37 #, fuzzy msgid "online" msgstr "En ligne" #: frontends/src/wix/constants.py:41 msgid "AFK" msgstr "Loin du clavier" #: frontends/src/wix/constants.py:42 msgid "DND" msgstr "Ne pas déranger" #: frontends/src/wix/contact_list.py:79 #, python-format msgid "update %s" msgstr "mise à jour de %s" #: frontends/src/wix/contact_list.py:153 #, fuzzy, python-format msgid "adding %s" msgstr "Ajout de %s" #: frontends/src/wix/contact_list.py:192 #, fuzzy, python-format msgid "removing %s" msgstr "Ajout de %s" #: frontends/src/wix/main_window.py:87 msgid "Wix jabber client" msgstr "client jabber Wix" #: frontends/src/wix/main_window.py:109 #, python-format msgid "plugin profile %s" msgstr "branchement du profil %s" #: frontends/src/wix/main_window.py:118 #, fuzzy msgid "Creating menus" msgstr "Construction du jeu de Tarot" #: frontends/src/wix/main_window.py:120 msgid "&Connect\tCTRL-c" msgstr "&Connexion\tCTRL-c" #: frontends/src/wix/main_window.py:120 #, fuzzy msgid " Connect to the server" msgstr "Connexion au démarrage des frontends" #: frontends/src/wix/main_window.py:121 msgid "&Disconnect\tCTRL-d" msgstr "&Déconnexion\tCTRL-d" #: frontends/src/wix/main_window.py:121 #, fuzzy msgid " Disconnect from the server" msgstr "Déconnexion à la fermeture des frontends" #: frontends/src/wix/main_window.py:122 msgid "&Parameters" msgstr "&Paramètres" #: frontends/src/wix/main_window.py:122 msgid " Configure the program" msgstr " Configurer l'application" #: frontends/src/wix/main_window.py:124 msgid "A&bout" msgstr "À propos" #: frontends/src/wix/main_window.py:124 #, python-format msgid " About %s" msgstr " À propos %s" #: frontends/src/wix/main_window.py:125 msgid "E&xit" msgstr "Quitter" #: frontends/src/wix/main_window.py:125 msgid " Terminate the program" msgstr " Ferme l'application" #: frontends/src/wix/main_window.py:127 msgid "&Add contact" msgstr "&Ajouter un contact" #: frontends/src/wix/main_window.py:127 msgid " Add a contact to your list" msgstr " Ajouter un contact à votre liste" #: frontends/src/wix/main_window.py:128 msgid "&Remove contact" msgstr "Supp&rimer un contact" #: frontends/src/wix/main_window.py:128 msgid " Remove the selected contact from your list" msgstr " Supprime le contact sélectionné de votre liste" #: frontends/src/wix/main_window.py:130 #, fuzzy msgid "&Show profile" msgstr "Affiche la barre de progression" #: frontends/src/wix/main_window.py:130 #, fuzzy msgid " Show contact's profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: frontends/src/wix/main_window.py:132 msgid "&Join Room" msgstr "Re&joindre un salon" #: frontends/src/wix/main_window.py:132 #, fuzzy msgid " Join a Multi-User Chat room" msgstr "" "Implémentation de l'initialisation de flux pour le transfert de fichier " #: frontends/src/wix/main_window.py:134 msgid "&General" msgstr "&Général" #: frontends/src/wix/main_window.py:135 msgid "&Contacts" msgstr "&Contacts" #: frontends/src/wix/main_window.py:136 #, fuzzy msgid "&Communication" msgstr "Connexion..." #: frontends/src/wix/main_window.py:267 #, fuzzy msgid "Confirmation asked" msgstr "inscription demandée pour" #: frontends/src/wix/main_window.py:270 msgid "File transfer confirmation asked" msgstr "Demande de confirmation pour un transfer de fichier demandée" #: frontends/src/wix/main_window.py:272 msgid "File Request" msgstr "Gestion de fichiers" #: frontends/src/wix/main_window.py:290 msgid "Yes/No confirmation asked" msgstr "confirmation de type Oui/Non demandée" #: frontends/src/wix/main_window.py:292 #: frontends/src/wix/profile_manager.py:111 #, fuzzy msgid "Confirmation" msgstr "Connexion..." #: frontends/src/wix/main_window.py:306 #, python-format msgid "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" msgstr "actionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]" #: frontends/src/wix/main_window.py:315 msgid "Success" msgstr "Succès" #: frontends/src/wix/main_window.py:334 #, fuzzy msgid "registration" msgstr "enregistrement" #: frontends/src/wix/main_window.py:382 #, python-format msgid "onContactActivated: %s" msgstr "onContactActivated: %s" #: frontends/src/wix/main_window.py:400 msgid "Status change request" msgstr "Demande de changement de statut" #: frontends/src/wix/main_window.py:404 msgid "Param request" msgstr "Gestion des paramètres" #: frontends/src/wix/main_window.py:406 #, fuzzy msgid "Configuration" msgstr "Connexion..." #: frontends/src/wix/main_window.py:422 #, python-format msgid "%(name)s is a SàT (Salut à Toi) frontend\n" msgstr "%(name)s est un frontend pour SàT (Salut à Toi)\n" #: frontends/src/wix/main_window.py:438 msgid "Add contact request" msgstr "Demande d'ajout de contact" #: frontends/src/wix/main_window.py:441 msgid "name@server.tld" msgstr "nom@serveur.ext" #: frontends/src/wix/main_window.py:454 msgid "Remove contact request" msgstr "Demande de suppression de contact" #: frontends/src/wix/main_window.py:457 frontends/src/wix/main_window.py:480 msgid "You haven't selected any contact !" msgstr "Vous n'avez sélectionné aucun contact !" #: frontends/src/wix/main_window.py:465 #, python-format msgid "Are you sure you want to delete %s from your roster list ?" msgstr "Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?" #: frontends/src/wix/main_window.py:466 msgid "Contact suppression" msgstr "Suppression de contact" #: frontends/src/wix/main_window.py:477 msgid "Show contact's profile request" msgstr "Demande d'affichage du profile d'un contact" #: frontends/src/wix/main_window.py:493 #, fuzzy, python-format msgid "Profile received: [%s]" msgstr "tarot: chien reçu" #: frontends/src/wix/main_window.py:512 #, fuzzy msgid "Exiting..." msgstr "Déconnexion..." #: frontends/src/wix/main_window.py:519 msgid "Tray Click" msgstr "Clic sur l'icône de la barre de tâches" #: frontends/src/wix/profile_manager.py:46 msgid "Profile:" msgstr "Profile:" #: frontends/src/wix/profile_manager.py:59 msgid "Login" msgstr "Identifiant" #: frontends/src/wix/profile_manager.py:95 msgid "Please enter the new profile name" msgstr "Veuillez entrer le nom du nouveau profile" #: frontends/src/wix/profile_manager.py:111 #, python-format msgid "Are you sure to delete the profile [%s]" msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" #: frontends/src/wix/profile_manager.py:131 msgid "You must select a profile or create a new one before connecting" msgstr "" "Vous devez sélectionner un profile ou en créer un nouveau avant de vous " "connecter." #: frontends/src/wix/profile_manager.py:148 #, fuzzy msgid "Saving new JID and server" msgstr "Sauvegarde du nouveau JID" #: frontends/src/wix/profile_manager.py:152 msgid "Saving new password" msgstr "Sauvegarde du nouveau mot de passe" #: frontends/src/wix/profile_manager.py:158 #, fuzzy msgid "Profile error" msgstr "Mauvais nom de profile" #: frontends/src/wix/profile.py:34 msgid "Full Name" msgstr "Nom complet" #: frontends/src/wix/profile.py:35 msgid "Nickname" msgstr "Surnon" #: frontends/src/wix/profile.py:36 msgid "Birthday" msgstr "Date de naissance" #: frontends/src/wix/profile.py:37 msgid "Phone #" msgstr "N° de Tél:" #: frontends/src/wix/profile.py:38 msgid "Website" msgstr "Site Web" #: frontends/src/wix/profile.py:39 msgid "E-mail" msgstr "Courriel" #: frontends/src/wix/profile.py:40 msgid "Avatar" msgstr "Avatar" #: frontends/src/wix/profile.py:88 frontends/src/wix/xmlui.py:297 msgid "close" msgstr "fermeture" #: frontends/src/wix/quiz_game.py:168 msgid "" "Quel dommage, personne n'a trouvé la réponse\n" "\n" "Attention, la prochaine question arrive..." msgstr "" #: frontends/src/wix/xmlui.py:137 #, fuzzy, python-format msgid "Can't find value [%s] to select" msgstr "Impossible de trouver la VCard de %s" #: frontends/src/wix/xmlui.py:175 msgid "select" msgstr "" #: src/core/sat_main.py:97 #, python-format msgid "Constant %(name)s overrided with [%(value)s]" msgstr "" #: src/core/sat_main.py:102 msgid "Trying to access an undefined constant" msgstr "Vous essayer d'utiliser une constante indéfinie" #: src/core/sat_main.py:109 #, fuzzy msgid "Trying to redefine a constant" msgstr "Vous essayer d'utiliser une constante indéfinie" #: src/core/sat_main.py:177 #, fuzzy msgid "Memory initialised" msgstr "Le flux XML est initialisé" #: src/core/sat_main.py:207 #, python-format msgid "Dependency plugin not found: [%s]" msgstr "" #: src/core/sat_main.py:214 #, python-format msgid "importing plugin: %s" msgstr "Importation du plugin: %s" #: src/core/sat_main.py:233 msgid "Trying to connect a non-exsitant profile" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: src/core/sat_main.py:237 #, fuzzy msgid "already connected !" msgstr "Vous n'êtes pas connecté !" #: src/core/sat_main.py:255 #, 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" #: src/core/sat_main.py:283 msgid "setting plugins parents" msgstr "Configuration des parents des extensions" #: src/core/sat_main.py:304 msgid "not connected !" msgstr "Vous n'êtes pas connecté !" #: src/core/sat_main.py:307 #, fuzzy msgid "Disconnecting..." msgstr "Déconnexion..." #: src/core/sat_main.py:317 src/memory/memory.py:1027 msgid "Asking contacts for a non-existant profile" msgstr "Demande de contacts pour un profile inexistant" #: src/core/sat_main.py:328 #, fuzzy msgid "Asking group's contacts for a non-existant profile" msgstr "Demande de contacts pour un profile inexistant" #: src/core/sat_main.py:337 msgid "Trying to remove reference to a client not referenced" msgstr "" #: src/core/sat_main.py:349 msgid "running app" msgstr "Lancement de l'application" #: src/core/sat_main.py:353 msgid "stopping app" msgstr "Arrêt de l'application" #: src/core/sat_main.py:414 msgid "No user, password or server given, can't register new account." msgstr "" "L'utilisateur, le mot de passe ou le serveur n'ont pas été spécifiés, " "impossible d'inscrire un nouveau compte." #: src/core/sat_main.py:422 #, python-format msgid "Are you sure to register new account [%(user)s] to server %(server)s ?" msgstr "" "Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur " "%(server)s ?" #: src/core/sat_main.py:427 #, fuzzy, python-format msgid "register Confirmation CB ! (%s)" msgstr "Callback de confirmation d'inscription !" #: src/core/sat_main.py:442 #, python-format msgid "setting param: %(name)s=%(value)s in category %(category)s" msgstr "" "Le paramètre %(name)s vaut désormais %(value)s dans la catégorie %(category)s" #: src/core/sat_main.py:452 msgid "asking connection status for a non-existant profile" msgstr "demande de l'état de connexion pour un profile qui n'existe pas" #: src/core/sat_main.py:518 #, fuzzy, python-format msgid "Sending jabber message of type [%(type)s] to %(to)s..." msgstr "Envoi du message jabber à %s" #: src/core/sat_main.py:556 #, fuzzy msgid "Trying to send a message with no profile" msgstr "Tentative d'accès à un profile inconnu" #: src/core/sat_main.py:603 #, fuzzy, python-format msgid "subsciption request [%(subs_type)s] for %(jid)s" msgstr "demande d'inscription [%(type)s] pour %(jid)s" #: src/core/sat_main.py:716 src/core/sat_main.py:733 #, python-format msgid "Requested disco info on %s" msgstr "" #: src/core/sat_main.py:718 #, python-format msgid "Feature found: %s" msgstr "Fonctionnalité trouvée: %s" #: src/core/sat_main.py:721 #, python-format msgid "Identity found: [%(category)s/%(type)s] %(identity)s" msgstr "Identité trouvée: [%(category)s/%(type)s] %(identity)s" #: src/core/sat_main.py:735 #, fuzzy, python-format msgid "Identity added: (%(category)s,%(type)s) ==> %(entity)s [%(profile)s]" msgstr "Identité trouvée: [%(category)s/%(type)s] %(identity)s" #: src/core/sat_main.py:742 #, python-format msgid "" "Can't get information on identity [%(entity)s] for profile [%(profile)s]" msgstr "" #: src/core/sat_main.py:747 msgid "" "Using jabberfr workaround, be sure your domain has at least two levels (e.g. " "\"example.tld\", not \"example\" alone)" msgstr "" #: src/core/sat_main.py:771 #, fuzzy msgid "action_type for actionResultExt must be DICT_DICT, fixing it" msgstr "Le type pour actionResultExt doit être DICT_DICT, correction" #: src/core/sat_main.py:785 #, fuzzy msgid "Asking confirmation a non-existant profile" msgstr "Demande de contacts pour un profile inexistant" #: src/core/sat_main.py:787 msgid "Attempt to register two callbacks for the same confirmation" msgstr "Tentative de déclaration de 2 callbacks pour la même configuration" #: src/core/sat_main.py:796 #, fuzzy msgid "Confirmation answer from a non-existant profile" msgstr "demande de l'état de connexion pour un profile qui n'existe pas" #: src/core/sat_main.py:797 #, fuzzy, python-format msgid "Received confirmation answer for conf_id [%(conf_id)s]: %(success)s" msgstr "Réponse pour confirmation reçu (id [%(id)s]): %(success)s" #: src/core/sat_main.py:797 msgid "accepted" msgstr "accepté" #: src/core/sat_main.py:797 msgid "refused" msgstr "refusé" #: src/core/sat_main.py:799 #, fuzzy, python-format msgid "Received an unknown confirmation (%(id)s for %(profile)s)" msgstr "Confirmation inconnue reçue" #: src/core/sat_main.py:818 msgid "Trying to remove an unknow progress callback" msgstr "Tentative d'effacement d'une callback de progression inconnue." #: src/core/sat_main.py:851 #, fuzzy msgid "id already registered" msgstr "Vous êtes maintenant désinscrit" #: src/core/sat_main.py:880 #, fuzzy msgid "trying to launch action with a non-existant profile" msgstr "Tentative d'ajout d'un contact à un profile inexistant" #: src/core/sat_main.py:935 #, fuzzy msgid "A menu with the same path and type already exists" msgstr "Ce nom de profile existe déjà" #: src/core/xmpp.py:57 #, python-format msgid "********** [%s] CONNECTED **********" msgstr "********** [%s] CONNECTÉ **********" #: src/core/xmpp.py:63 msgid "XML stream is initialized" msgstr "Le flux XML est initialisé" #: src/core/xmpp.py:100 #, python-format msgid "********** [%s] DISCONNECTED **********" msgstr "********** [%s] DÉCONNECTÉ **********" #: src/core/xmpp.py:104 msgid "No keep_alife" msgstr "Pas de \"keep_alife\"" #: src/core/xmpp.py:120 #, python-format msgid "got message from: %s" msgstr "message reçu de: %s" #: src/core/xmpp.py:224 #, python-format msgid "new contact in roster list: %s" msgstr "nouveau contact: %s" #: src/core/xmpp.py:235 #, python-format msgid "removing %s from roster list" msgstr "supppression du contact %s" #: src/core/xmpp.py:293 #, fuzzy, python-format msgid "" "presence update for [%(entity)s] (available, show=%(show)s statuses=" "%(statuses)s priority=%(priority)d)" msgstr "" "Mise à jour de l'information de présence pour [%(entity)s] (unavailable, " "statuses=%(statuses)s)" #: src/core/xmpp.py:312 #, 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)" #: src/core/xmpp.py:355 #, fuzzy msgid "sending automatic \"from\" subscription request" msgstr "envoi automatique de la demande d'inscription \"to\"" #: src/core/xmpp.py:363 #, python-format msgid "subscription approved for [%s]" msgstr "inscription approuvée pour [%s]" #: src/core/xmpp.py:367 #, fuzzy, python-format msgid "unsubscription confirmed for [%s]" msgstr "demande de désinscription pour [%s]" #: src/core/xmpp.py:371 #, fuzzy, python-format msgid "subscription request from [%s]" msgstr "inscription approuvée pour [%s]" #: src/core/xmpp.py:375 #, fuzzy msgid "sending automatic subscription acceptance" msgstr "envoi automatique de la demande d'inscription \"to\"" #: src/core/xmpp.py:382 #, python-format msgid "unsubscription asked for [%s]" msgstr "demande de désinscription pour [%s]" #: src/core/xmpp.py:385 #, fuzzy msgid "automatic contact deletion" msgstr "Sélection du contrat" #: src/core/xmpp.py:417 #, fuzzy msgid "Registration asked for" msgstr "Éched de l'insciption (%s)" #: src/core/xmpp.py:438 src/plugins/plugin_xep_0077.py:90 #, python-format msgid "registration answer: %s" msgstr "réponse à la demande d'inscription: %s" #: src/core/xmpp.py:440 msgid "Registration successfull" msgstr "Inscription réussie" #: src/core/xmpp.py:445 src/plugins/plugin_xep_0077.py:86 #: src/plugins/plugin_xep_0077.py:96 #, python-format msgid "Registration failure: %s" msgstr "Échec de l'inscription: %s" #: src/core/xmpp.py:450 src/plugins/plugin_xep_0077.py:98 msgid "Username already exists, please choose an other one" msgstr "Ce nom d'utilisateur existe déjà, veuillez en choisir un autre" #: src/core/xmpp.py:453 #, python-format msgid "Registration failed (%s)" msgstr "Éched de l'insciption (%s)" #: src/memory/memory.py:143 #, fuzzy msgid "Connection" msgstr "Connexion..." #: src/memory/memory.py:144 msgid "Register new account" msgstr "Enregistrement d'un nouveau compte" #: src/memory/memory.py:145 #, fuzzy msgid "Connect on frontend startup" msgstr "Connexion au démarrage des frontends" #: src/memory/memory.py:146 #, fuzzy msgid "Disconnect on frontend closure" msgstr "Déconnexion à la fermeture des frontends" #: src/memory/memory.py:147 src/plugins/plugin_xep_0249.py:69 msgid "Misc" msgstr "Divers" #: src/memory/memory.py:202 #, fuzzy, python-format msgid "Trying to purge cache of a profile not in memory: [%s]" msgstr "Tentative d'appel d'un profile inconnue" #: src/memory/memory.py:223 #, fuzzy, python-format msgid "The profile [%s] already exists" msgstr "Ce nom de profile existe déjà" #: src/memory/memory.py:239 #, fuzzy msgid "The profile name already exists" msgstr "Ce nom de profile existe déjà" #: src/memory/memory.py:250 #, fuzzy msgid "Trying to delete an unknown profile" msgstr "Tentative d'accès à un profile inconnu" #: src/memory/memory.py:253 #, fuzzy msgid "Trying to delete a connected profile" msgstr "Tentative de suppression d'un contact pour un profile inexistant" #: src/memory/memory.py:268 msgid "No default profile, returning first one" msgstr "Pas de profile par défaut, envoi du premier" #: src/memory/memory.py:272 #, fuzzy msgid "No profile exist yet" msgstr "Aucun profile sélectionné" #: src/memory/memory.py:280 msgid "Trying to access an unknown profile" msgstr "Tentative d'accès à un profile inconnu" #: src/memory/memory.py:361 msgid "Trying to register frontends parameters with no specified app: aborted" msgstr "" #: src/memory/memory.py:366 #, python-format msgid "Trying to register twice frontends parameters for %(app)s: aborted" msgstr "" #: src/memory/memory.py:377 #, 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" #: src/memory/memory.py:392 src/memory/memory.py:437 src/memory/memory.py:472 #, 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 !" #: src/memory/memory.py:448 msgid "Requesting a param for an non-existant profile" msgstr "Demande d'un paramètre pour un profile inconnu" #: src/memory/memory.py:452 #, fuzzy msgid "Requesting synchronous param for not connected profile" msgstr "Demande d'un paramètre pour un profile inconnu" #: src/memory/memory.py:476 #, python-format msgid "" "Trying to get parameter '%(param)s' in category '%(cat)s' without " "authorization!!!" msgstr "" #: src/memory/memory.py:488 #, fuzzy msgid "Requesting a param for a non-existant profile" msgstr "Demande d'un paramètre pour un profile inconnu" #: src/memory/memory.py:622 src/memory/memory.py:638 src/memory/memory.py:661 msgid "Asking params for inexistant profile" msgstr "Demande de paramètres pour un profile inconnu" #: src/memory/memory.py:713 #, fuzzy msgid "Trying to set parameter for an unknown profile" msgstr "Tentative d'accès à un profile inconnu" #: src/memory/memory.py:718 #, python-format msgid "Requesting an unknown parameter (%(category)s/%(name)s)" msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" #: src/memory/memory.py:723 #, python-format msgid "" "Trying to set parameter '%(param)s' in category '%(cat)s' without " "authorization!!!" msgstr "" #: src/memory/memory.py:776 msgid "Memory manager init" msgstr "Initialisation du gestionnaire de mémoire" #: src/memory/memory.py:791 #, fuzzy msgid "Loading default params template" msgstr "Impossible de charger le modèle des paramètres !" #: src/memory/memory.py:804 msgid "Can't read main config !" msgstr "" #: src/memory/memory.py:838 #, python-format msgid "Parameters loaded from file: %s" msgstr "" #: src/memory/memory.py:841 #, fuzzy, python-format msgid "Can't load parameters from file: %s" msgstr "Impossible de charger le modèle des paramètres !" #: src/memory/memory.py:857 #, python-format msgid "[%s] Profile session started" msgstr "" #: src/memory/memory.py:863 #, fuzzy, python-format msgid "[%s] Profile session purge" msgstr "Ce profile n'est pas utilisé" #: src/memory/memory.py:868 #, python-format msgid "Trying to purge roster status cache for a profile not in memory: [%s]" msgstr "" #: src/memory/memory.py:878 #, fuzzy, python-format msgid "Parameters saved to file: %s" msgstr "Échec de la désinscription: %s" #: src/memory/memory.py:881 #, fuzzy, python-format msgid "Can't save parameters to file: %s" msgstr "Impossible de charger le modèle des paramètres !" #: src/memory/memory.py:967 #, python-format msgid "" "Entities (%(category)s/%(type)s) of %(server)s not available, maybe they " "haven't been asked yet?" msgstr "" #: src/memory/memory.py:996 #, fuzzy msgid "Trying find server feature for a non-existant profile" msgstr "Tentative d'ajout d'informations de présence à un profile inexistant" #: src/memory/memory.py:1004 #, python-format msgid "Features of %s not available, maybe they haven't been asked yet?" msgstr "" #: src/memory/memory.py:1013 #, fuzzy msgid "Asking contacts for a non-existant or not connected profile" msgstr "Demande de vcard pour un profile inexistant ou non connecté" #: src/memory/memory.py:1017 msgid "Entity not in cache" msgstr "" #: src/memory/memory.py:1041 msgid "Trying to add presence status to a non-existant profile" msgstr "Tentative d'ajout d'informations de présence à un profile inexistant" #: src/memory/memory.py:1061 src/memory/memory.py:1092 #, fuzzy msgid "Trying to get entity data for a non-existant profile" msgstr "Tentative de suppression d'un contact pour un profile inexistant" #: src/memory/memory.py:1135 msgid "Asking waiting subscriptions for a non-existant profile" msgstr "Demande des inscriptions en attente pour un profile inexistant" #: src/memory/persistent.py:38 msgid "PersistentDict can't be used before memory initialisation" msgstr "" #: src/memory/sqlite.py:83 msgid "Connecting database" msgstr "" #: src/memory/sqlite.py:88 msgid "Can't activate foreign keys" msgstr "" #: src/memory/sqlite.py:91 #, fuzzy msgid "The database is new, creating the tables" msgstr "Ce nom de profile existe déjà" #: src/memory/sqlite.py:162 #, fuzzy, python-format msgid "Can't delete profile [%s]" msgstr "Êtes vous sûr de vouloir supprimer le profile [%s] ?" #: src/memory/sqlite.py:167 #, fuzzy, python-format msgid "Profile [%s] deleted" msgstr "Aucun profile sélectionné" #: src/memory/sqlite.py:181 #, fuzzy msgid "loading general parameters from database" msgstr "Impossible de charger les paramètres généraux !" #: src/memory/sqlite.py:194 #, fuzzy msgid "loading individual parameters from database" msgstr "Impossible de charger les paramètres individuels !" #: src/memory/sqlite.py:216 #, fuzzy, python-format msgid "Can't set general parameter (%(category)s/%(name)s) in database" msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" #: src/memory/sqlite.py:227 #, fuzzy, python-format msgid "" "Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] in " "database" msgstr "" "Impossible de déterminer la valeur par défaut pour [%(category)s/%(name)s]: " "%(reason)s" #: src/memory/sqlite.py:247 #, python-format msgid "" "Can't save following message in history: from [%(from_jid)s] to [%(to_jid)s] " "==> [%(message)s]" msgstr "" #: src/memory/sqlite.py:312 #, python-format msgid "loading general private values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:314 src/memory/sqlite.py:330 #, python-format msgid "No data present in database for namespace %s" msgstr "" #: src/memory/sqlite.py:327 #, python-format msgid "loading individual private values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:339 #, python-format msgid "" "Can't set general private value (%(key)s) [namespace:%(namespace)s] in " "database" msgstr "" #: src/memory/sqlite.py:351 #, python-format msgid "" "Can't set individual private value (%(key)s) [namespace: %(namespace)s] for " "[%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:361 #, python-format msgid "" "Can't delete general private value (%(key)s) [namespace:%(namespace)s] in " "database" msgstr "" #: src/memory/sqlite.py:372 #, python-format msgid "" "Can't delete individual private value (%(key)s) [namespace: %(namespace)s] " "for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:386 #, python-format msgid "loading general private binary values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:388 src/memory/sqlite.py:404 #, python-format msgid "No binary data present in database for namespace %s" msgstr "" #: src/memory/sqlite.py:401 #, python-format msgid "loading individual private binary values [namespace: %s] from database" msgstr "" #: src/memory/sqlite.py:413 #, python-format msgid "" "Can't set general private binary value (%(key)s) [namespace:%(namespace)s] " "in database" msgstr "" #: src/memory/sqlite.py:425 #, python-format msgid "" "Can't set individual binary private value (%(key)s) [namespace: " "%(namespace)s] for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:435 #, python-format msgid "" "Can't delete general private binary value (%(key)s) [namespace:" "%(namespace)s] in database" msgstr "" #: src/memory/sqlite.py:446 #, python-format msgid "" "Can't delete individual private binary value (%(key)s) [namespace: " "%(namespace)s] for [%(profile)s] in database" msgstr "" #: src/memory/sqlite.py:512 msgid "" "Your local schema is up-to-date, but database versions mismatch, fixing it..." msgstr "" #: src/memory/sqlite.py:522 msgid "" "There is a schema mismatch, but as we are on a dev version, database will be " "updated" msgstr "" #: src/memory/sqlite.py:526 msgid "" "schema version is up-to-date, but local schema differ from expected current " "schema" msgstr "" #: src/memory/sqlite.py:528 #, 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 "" #: src/memory/sqlite.py:532 msgid "Database schema has changed, local database will be updated" msgstr "" #: src/plugins/deprecated_misc_cs.py:50 msgid "" "This plugin allow to manage your CouchSurfing account throught your SàT " "frontend" msgstr "" "Cette extension vous permet de gérer votre compte CouchSurfing à travers " "votre frontend SàT" #: src/plugins/deprecated_misc_cs.py:70 #, fuzzy msgid "Plugin CS initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/deprecated_misc_cs.py:75 msgid "Plugin" msgstr "Extension" #: src/plugins/deprecated_misc_cs.py:75 #, fuzzy msgid "Launch CoushSurfing management interface" msgstr "Lancement de l'interface de gestion de CouchSurfing" #: src/plugins/deprecated_misc_cs.py:95 msgid "" "Impossible to contact CS website, please check your login/password, " "connection or try again later" msgstr "" "Impossible de contacter le site CouchSurfing, veuillez vérifier vos " "identifiant/mot de passe, votre connexion, ou essayez un peu plus tard" #: src/plugins/deprecated_misc_cs.py:103 msgid "" "You have to fill your CouchSurfing login & password in parameters before " "using this interface" msgstr "" "Vous devez remplir vos identifiant & mot de passe CouchSurfing dans les " "paramètres avant d'utiliser cette interface" #: src/plugins/deprecated_misc_cs.py:162 msgid "Messages" msgstr "Messages" #: src/plugins/deprecated_misc_cs.py:163 #, python-format msgid "" "G'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and " "%(unread_CR_mess)s unread couch request message%(plural_CR)s\n" "If you want to send a message, select the recipient(s) in the list below" msgstr "" "Bonjour %(name)s, vous avez %(nb_message)i message%(plural_mess)s non lus et " "%(unread_CR_mess)s requête%(plural_CR)s d'hébergement en attente\n" "Si vous voulez envoyer un message, sélectionnez le(s) destinataire(s) dans " "la liste ci-dessous." #: src/plugins/deprecated_misc_cs.py:165 #, python-format msgid "Show unread message%(plural)s in external web browser" msgstr "Afficher le%(plural)s message%(plural)s non lu dans un navigateur web" #: src/plugins/deprecated_misc_cs.py:168 msgid "Subject" msgstr "Sujet" #: src/plugins/deprecated_misc_cs.py:171 msgid "Message" msgstr "Message" #: src/plugins/deprecated_misc_cs.py:174 msgid "send" msgstr "envoyer" #: src/plugins/deprecated_misc_cs.py:195 #, python-format msgid "" "CS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" msgstr "" "Amis CS trouvé: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)" #: src/plugins/deprecated_misc_cs.py:218 msgid "" "INTERNAL ERROR: no confirmation of message sent by CS, maybe the site has " "been modified ?" msgstr "" "ERREUR INTERNE: aucune confirmation du message envoyée par CS, peut être que " "le site a été modifié ?" #: src/plugins/deprecated_misc_cs.py:229 #, fuzzy, python-format msgid "Sending message to %s" msgstr "Envoi du message jabber à %s" #: src/plugins/deprecated_misc_cs.py:230 #, python-format msgid "" "\n" "subject: %(subject)s\n" "message: \n" "---\n" "%(message)s\n" "---\n" "\n" msgstr "" "\n" "sujet: %(subject)s\n" "message: \n" "---\n" "%(message)s\n" "---\n" "\n" #: src/plugins/deprecated_misc_cs.py:236 msgid "Message sent" msgstr "Message envoyé" #: src/plugins/deprecated_misc_cs.py:237 msgid "The message has been sent to every recipients" msgstr "Le message a été envoyé à tous les destinataires" #: src/plugins/deprecated_misc_cs.py:250 msgid "There is not recipient selected for this message !" msgstr "Il n'y a aucun destinataire pour ce message !" #: src/plugins/deprecated_misc_cs.py:256 #, python-format msgid "sending message to %(friends)s with subject [%(subject)s]" msgstr "Envoi du message à %(friends)s avec le sujet [%(subject)s]" #: src/plugins/plugin_adhoc_dbus.py:48 #, fuzzy msgid "Add D-Bus management to Ad-Hoc commands" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_adhoc_dbus.py:55 #, fuzzy msgid "plugin Ad-Hoc D-Bus initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_adhoc_dbus.py:164 #, fuzzy msgid "Command selection" msgstr "Sélection du contrat" #: src/plugins/plugin_adhoc_dbus.py:201 src/plugins/plugin_xep_0050.py:372 #, fuzzy msgid "Updated" msgstr "mise à jour de %s" #: src/plugins/plugin_adhoc_dbus.py:205 msgid "Command sent" msgstr "" #: src/plugins/plugin_exp_command_export.py:36 #, fuzzy msgid "Implementation of command export" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_exp_command_export.py:86 #, fuzzy msgid "Plugin command export initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_exp_parrot.py:35 msgid "Implementation of parrot mode (repeat messages between 2 entities)" msgstr "" #: src/plugins/plugin_exp_parrot.py:47 #, fuzzy msgid "Plugin Parrot initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_exp_pipe.py:43 #, fuzzy msgid "Implementation of SI Pipe Transfer" msgstr "" "Implémentation de l'initialisation de flux pour le transfert de fichier " #: src/plugins/plugin_exp_pipe.py:51 #, fuzzy msgid "Plugin Pipe initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_exp_pipe.py:66 #, python-format msgid "SI Pipe Transfer: TimeOut reached for id %s" msgstr "" #: src/plugins/plugin_exp_pipe.py:71 src/plugins/plugin_xep_0096.py:70 #, fuzzy msgid "kill id called on a non existant approval id" msgstr "Demande d'un contact pour un profile inexistant" #: src/plugins/plugin_exp_pipe.py:81 msgid "EXP-PIPE file transfer requested" msgstr "" #: src/plugins/plugin_exp_pipe.py:90 #, fuzzy msgid "No pipe element found" msgstr "Aucun profile sélectionné" #: src/plugins/plugin_exp_pipe.py:100 src/plugins/plugin_xep_0096.py:117 #, fuzzy msgid "No stream method found" msgstr "Aucune donnée trouvée" #: src/plugins/plugin_exp_pipe.py:104 src/plugins/plugin_xep_0096.py:121 msgid "Can't find a valid stream method" msgstr "" #: src/plugins/plugin_exp_pipe.py:108 src/plugins/plugin_xep_0096.py:125 #, fuzzy msgid "No feature element found" msgstr "Aucune donnée trouvée" #: src/plugins/plugin_exp_pipe.py:133 src/plugins/plugin_xep_0096.py:159 msgid "dest path not found in frontend_data" msgstr "" #: src/plugins/plugin_exp_pipe.py:143 src/plugins/plugin_xep_0096.py:171 msgid "" "Unknown stream method, this should not happen at this stage, cancelling " "transfer" msgstr "" #: src/plugins/plugin_exp_pipe.py:153 src/plugins/plugin_xep_0096.py:186 #, python-format msgid "Transfer [%s] refused" msgstr "Transfert [%s] refusé" #: src/plugins/plugin_exp_pipe.py:164 src/plugins/plugin_exp_pipe.py:249 #: src/plugins/plugin_xep_0096.py:197 #, fuzzy, python-format msgid "Transfer %s successfuly finished" msgstr "Transfert [%s] refusé" #: src/plugins/plugin_exp_pipe.py:175 #, python-format msgid "Transfer %(id)s failed with stream method %(s_method)s" msgstr "" #: src/plugins/plugin_exp_pipe.py:180 src/plugins/plugin_xep_0096.py:216 msgid "All stream methods failed, can't transfer the file" msgstr "" #: src/plugins/plugin_exp_pipe.py:187 #, fuzzy, python-format msgid "Pipe transfer refused by %s" msgstr "Transfert [%s] refusé" #: src/plugins/plugin_exp_pipe.py:188 #, fuzzy, python-format msgid "The contact %s refused your pipe stream" msgstr "Le contact %s a refusé votre inscription" #: src/plugins/plugin_exp_pipe.py:188 msgid "Pipe stream refused" msgstr "" #: src/plugins/plugin_exp_pipe.py:190 #, python-format msgid "Error during pipe stream transfer with %s" msgstr "" #: src/plugins/plugin_exp_pipe.py:191 #, python-format msgid "" "Something went wrong during the pipe stream session intialisation with %s" msgstr "" #: src/plugins/plugin_exp_pipe.py:191 msgid "Pipe stream error" msgstr "" #: src/plugins/plugin_exp_pipe.py:197 src/plugins/plugin_xep_0096.py:233 msgid "Protocol error during file transfer" msgstr "" #: src/plugins/plugin_exp_pipe.py:202 src/plugins/plugin_xep_0096.py:238 msgid "No feature element" msgstr "" #: src/plugins/plugin_exp_pipe.py:209 src/plugins/plugin_xep_0096.py:245 #, fuzzy msgid "No stream method choosed" msgstr "Contact choisi: %s" #: src/plugins/plugin_exp_pipe.py:223 src/plugins/plugin_xep_0096.py:267 msgid "Invalid stream method received" msgstr "" #: src/plugins/plugin_exp_pipe.py:235 src/plugins/plugin_xep_0096.py:280 #, fuzzy msgid "Trying to send a file from an unknown profile" msgstr "Tentative d'accès à un profile inconnu" #: src/plugins/plugin_exp_pipe.py:254 #, python-format msgid "Transfer %(id)s failed with stream method %(s_method)s %(profile)s" msgstr "" #: src/plugins/plugin_misc_account.py:38 msgid "SàT account creation" msgstr "" #: src/plugins/plugin_misc_account.py:79 #, fuzzy msgid "Prosody registration success" msgstr "Inscription réussie" #: src/plugins/plugin_misc_account.py:82 #, python-format msgid "Can't register Prosody account (error code: %(code)d): %(message)s" msgstr "" #: src/plugins/plugin_misc_account.py:92 #, fuzzy msgid "Plugin Account initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_account.py:100 #, fuzzy, python-format msgid "Can't find %s" msgstr "Impossible de trouver la VCard de %s" #: src/plugins/plugin_misc_account.py:103 #, fuzzy, python-format msgid "Prosody path found: %s" msgstr "Fonctionnalité trouvée: %s" #: src/plugins/plugin_misc_groupblog.py:61 #, fuzzy msgid "Implementation of microblogging with roster access" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_misc_groupblog.py:85 #, fuzzy msgid "Group blog plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_misc_groupblog.py:147 #: src/plugins/plugin_misc_room_game.py:422 #, python-format msgid "No client for this profile key: %s" msgstr "" #: src/plugins/plugin_misc_groupblog.py:153 msgid "Looking for item-access power pubsub server" msgstr "" #: src/plugins/plugin_misc_groupblog.py:161 #, python-format msgid "item-access powered pubsub service found: [%s]" msgstr "" #: src/plugins/plugin_misc_groupblog.py:171 msgid "No item-access powered pubsub server found, can't use group blog" msgstr "" #: src/plugins/plugin_misc_groupblog.py:320 #, fuzzy msgid "Unknown access_type" msgstr "Type d'action inconnu" #: src/plugins/plugin_misc_groupblog.py:376 #, fuzzy msgid "Unknown access type" msgstr "Type d'action inconnu" #: src/plugins/plugin_misc_imap.py:46 msgid "" "Create an Imap server that you can use to read your \"normal\" type messages" msgstr "" #: src/plugins/plugin_misc_imap.py:64 #, fuzzy msgid "Plugin Imap Server initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_imap.py:71 #, fuzzy, python-format msgid "Launching IMAP server on port %d" msgstr "Lancement du serveur de flux Socks5 sur le port %d" #: src/plugins/plugin_misc_imap.py:439 #, fuzzy msgid "IMAP server connection started" msgstr "Connexion du serveur SOCKS 5 démarrée" #: src/plugins/plugin_misc_imap.py:442 #, fuzzy, python-format msgid "IMAP server connection lost (reason: %s)" msgstr "Connexion du serveur SOCKS5 perdue (raison: %s)" #: src/plugins/plugin_misc_maildir.py:47 msgid "Intercept \"normal\" type messages, and put them in a Maildir type box" msgstr "" #: src/plugins/plugin_misc_maildir.py:60 #, fuzzy msgid "Plugin Maildir initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_maildir.py:127 #, fuzzy msgid "Trying to remove an mailboxUser not referenced" msgstr "Tentative d'effacement d'une callback générale inconnue." #: src/plugins/plugin_misc_maildir.py:128 #: src/plugins/plugin_misc_maildir.py:147 #: src/plugins/plugin_misc_maildir.py:286 #: src/plugins/plugin_misc_maildir.py:290 #: src/plugins/plugin_misc_maildir.py:294 #, fuzzy msgid "INTERNAL ERROR: " msgstr "ERREUR INTERNE: paramètres xml non valides" #: src/plugins/plugin_misc_maildir.py:146 msgid "Boxname doesn't exist in internal data" msgstr "" #: src/plugins/plugin_misc_maildir.py:285 #, fuzzy msgid "Trying to remove an observer for an inexistant mailbox" msgstr "Tentative de suppression d'un contact pour un profile inexistant" #: src/plugins/plugin_misc_maildir.py:289 msgid "Trying to remove an inexistant observer, no observer for this signal" msgstr "" #: src/plugins/plugin_misc_maildir.py:293 #, fuzzy msgid "Trying to remove an inexistant observer" msgstr "Vous essayer de connecter un profile qui n'existe pas" #: src/plugins/plugin_misc_quiz.py:44 #, fuzzy msgid "Implementation of Quiz game" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_misc_quiz.py:56 #, fuzzy msgid "Plugin Quiz initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_quiz.py:182 #: src/plugins/plugin_misc_room_game.py:522 #: src/plugins/plugin_misc_room_game.py:545 #: src/plugins/plugin_misc_tarot.py:392 src/plugins/plugin_misc_tarot.py:406 #, python-format msgid "profile %s is unknown" msgstr "le profil %s est inconnu" #: src/plugins/plugin_misc_quiz.py:254 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 "" #: src/plugins/plugin_misc_quiz.py:282 src/plugins/plugin_misc_tarot.py:474 #, 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]" #: src/plugins/plugin_misc_quiz.py:330 src/plugins/plugin_misc_radiocol.py:231 #, fuzzy, python-format msgid "Unmanaged game element: %s" msgstr "élément de jeu de carte inconnu: %s" #: src/plugins/plugin_misc_radiocol.py:45 #, fuzzy msgid "Implementation of radio collective" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_misc_radiocol.py:63 #, fuzzy msgid "Radio collective initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_radiocol.py:101 msgid "Can't access profile's data" msgstr "" #: src/plugins/plugin_misc_radiocol.py:125 msgid "No more participants in the radiocol: cleaning data" msgstr "" #: src/plugins/plugin_misc_radiocol.py:161 msgid "INTERNAL ERROR: can't find full path of the song to delete" msgstr "" #: src/plugins/plugin_misc_radiocol.py:168 #, python-format msgid "INTERNAL ERROR: can't find %s on the file system" msgstr "" #: src/plugins/plugin_misc_room_game.py:45 msgid "Base class for MUC games" msgstr "" #: src/plugins/plugin_misc_room_game.py:197 #, python-format msgid "%(user)s not allowed to join the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:331 #, python-format msgid "%(user)s not allowed to invite for the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:376 #, python-format msgid "" "Still waiting for %(users)s before starting the game %(game)s in %(room)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:394 #, python-format msgid "Preparing room for %s game" msgstr "" #: src/plugins/plugin_misc_room_game.py:397 src/plugins/plugin_xep_0045.py:259 #, fuzzy msgid "Unknown profile" msgstr "Afficher profile" #: src/plugins/plugin_misc_room_game.py:503 #, 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" #: src/plugins/plugin_misc_room_game.py:506 #, python-format msgid "%(game)s game in room %(room)s can only be created by %(user)s" msgstr "" #: src/plugins/plugin_misc_room_game.py:519 #, fuzzy, python-format msgid "Creating %(game)s game in room %(room)s" msgstr "Construction du jeu de Tarot" #: src/plugins/plugin_misc_room_game.py:559 #, python-format msgid "new round for %s game" msgstr "" #: src/plugins/plugin_misc_room_game.py:624 msgid "Message can not be sent without a sender profile" msgstr "" #: src/plugins/plugin_misc_smtp.py:46 msgid "" "Create a SMTP server that you can use to send your \"normal\" type messages" msgstr "" #: src/plugins/plugin_misc_smtp.py:63 #, fuzzy msgid "Plugin SMTP Server initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_smtp.py:70 #, fuzzy, python-format msgid "Launching SMTP server on port %d" msgstr "Lancement du serveur de flux Socks5 sur le port %d" #: src/plugins/plugin_misc_smtp.py:96 #, fuzzy, python-format msgid "Can't send message: %s" msgstr "message reçu de: %s" #: src/plugins/plugin_misc_smtp.py:201 #, fuzzy msgid "SMTP server connection started" msgstr "Connexion du serveur SOCKS 5 démarrée" #: src/plugins/plugin_misc_smtp.py:205 #, fuzzy, python-format msgid "SMTP server connection lost (reason: %s)" msgstr "Connexion du serveur SOCKS5 perdue (raison: %s)" #: src/plugins/plugin_misc_tarot.py:43 #, fuzzy msgid "Implementation of Tarot card game" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_misc_tarot.py:55 #, fuzzy msgid "Plugin Tarot initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_tarot.py:60 msgid "Passe" msgstr "Passe" #: src/plugins/plugin_misc_tarot.py:60 msgid "Petite" msgstr "Petite" #: src/plugins/plugin_misc_tarot.py:60 msgid "Garde" msgstr "Garde" #: src/plugins/plugin_misc_tarot.py:60 msgid "Garde Sans" msgstr "Garde Sans" #: src/plugins/plugin_misc_tarot.py:60 msgid "Garde Contre" msgstr "Garde Contre" #: src/plugins/plugin_misc_tarot.py:102 msgid "contrat selection" msgstr "Sélection du contrat" #: src/plugins/plugin_misc_tarot.py:115 msgid "scores" msgstr "points" #: src/plugins/plugin_misc_tarot.py:196 src/plugins/plugin_misc_tarot.py:227 #, 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" #: src/plugins/plugin_misc_tarot.py:232 #, 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" #: src/plugins/plugin_misc_tarot.py:242 src/plugins/plugin_misc_tarot.py:314 #, 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" #: src/plugins/plugin_misc_tarot.py:292 #, fuzzy msgid "INTERNAL ERROR: contrat not managed (mispelled ?)" msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" #: src/plugins/plugin_misc_tarot.py:311 #, python-format msgid "" "The attacker (%(attaquant)s) makes %(points)i and needs to make " "%(point_limit)i (%(nb_bouts)s oulder%(plural)s%(separator)s%(bouts)s): he " "%(victory)s" msgstr "" "L'attaquant (%(attaquant)s) fait %(points)i et joue pour %(point_limit)i " "(%(nb_bouts)s bout%(plural)s%(separator)s%(bouts)s): il %(victory)s" #: src/plugins/plugin_misc_tarot.py:372 msgid "Internal error: unmanaged game stage" msgstr "ERREUR INTERNE: état de jeu inconnu" #: src/plugins/plugin_misc_tarot.py:394 #, python-format msgid "contrat [%(contrat)s] choosed by %(profile)s" msgstr "contrat [%(contrat)s] choisi par %(profile)s" #: src/plugins/plugin_misc_tarot.py:408 #, fuzzy, python-format msgid "Cards played by %(profile)s: [%(cards)s]" msgstr "Cartes jouées par %(profile)s: [%(cards)s]" #: src/plugins/plugin_misc_tarot.py:507 msgid "Everybody is passing, round ended" msgstr "" #: src/plugins/plugin_misc_tarot.py:514 #, python-format msgid "%(player)s win the bid with %(contrat)s" msgstr "%(player)s remporte l'enchère avec %(contrat)s" #: src/plugins/plugin_misc_tarot.py:535 #, fuzzy msgid "tarot: chien received" msgstr "tarot: chien reçu" #: src/plugins/plugin_misc_tarot.py:586 #, python-format msgid "The winner of this trick is %s" msgstr "le vainqueur de cette main est %s" #: src/plugins/plugin_misc_tarot.py:630 #, fuzzy, python-format msgid "Unmanaged error type: %s" msgstr "type d'erreur inconnu: %s" #: src/plugins/plugin_misc_tarot.py:632 #, python-format msgid "Unmanaged card game element: %s" msgstr "élément de jeu de carte inconnu: %s" #: src/plugins/plugin_misc_text_commands.py:32 msgid "IRC like text commands" msgstr "" #: src/plugins/plugin_misc_text_commands.py:42 #, fuzzy msgid "Text commands initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_misc_text_commands.py:230 msgid "Invalid jid, can't whois" msgstr "" #: src/plugins/plugin_misc_text_commands.py:233 #, python-format msgid "whois for %(jid)s" msgstr "" #: src/plugins/plugin_misc_text_commands.py:255 #, python-format msgid "" "Text commands available:\n" "%s" msgstr "" #: src/plugins/plugin_misc_text_syntaxes.py:33 src/test/constants.py:42 #, fuzzy msgid "Composition" msgstr "Connexion..." #: src/plugins/plugin_misc_text_syntaxes.py:58 msgid "Management of various text syntaxes (XHTML-IM, Markdown, etc)" msgstr "" #: src/plugins/plugin_misc_text_syntaxes.py:99 #, fuzzy msgid "Text syntaxes plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_misc_xmllog.py:33 msgid "Send raw XML logs to bridge" msgstr "" #: src/plugins/plugin_misc_xmllog.py:46 #, fuzzy msgid "INTERNAL ERROR: Unmanaged XML type" msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" #: src/plugins/plugin_misc_xmllog.py:65 #, fuzzy msgid "Activate XML log" msgstr "Lancement du flux" #: src/plugins/plugin_misc_xmllog.py:68 #, fuzzy msgid "Plugin XML Log initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_misc_xmllog.py:79 msgid "XML log activated" msgstr "" #: src/plugins/plugin_xep_0020.py:43 #, fuzzy msgid "Implementation of Feature Negotiation" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0020.py:50 #, fuzzy msgid "Plugin XEP_0020 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0020.py:71 #, python-format msgid "More than one value choosed for %s, keeping the first one" msgstr "" #: src/plugins/plugin_xep_0033.py:64 #, fuzzy msgid "Implementation of Extended Stanza Addressing" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0033.py:73 #, fuzzy msgid "Extended Stanza Addressing plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_xep_0033.py:88 msgid "XEP-0033 is being used but the server doesn't support it!" msgstr "" #: src/plugins/plugin_xep_0033.py:90 #, python-format msgid "" "Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!" msgstr "" #: src/plugins/plugin_xep_0033.py:91 msgid "TODO: addressing has be fixed by the backend... fix it in the frontend!" msgstr "" #: src/plugins/plugin_xep_0045.py:47 #, fuzzy msgid "Implementation of Multi-User Chat" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0045.py:58 #, fuzzy msgid "Plugin XEP_0045 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0045.py:83 #, python-format msgid "Unknown or disconnected profile (%s)" msgstr "Profil inconnu ou déconnecté (%s)" #: src/plugins/plugin_xep_0045.py:103 #, fuzzy msgid "Error while configuring the room" msgstr "Erreur en tentant de rejoindre le salon" #: src/plugins/plugin_xep_0045.py:114 #, fuzzy, python-format msgid "Error while joining the room %s" msgstr "Erreur en tentant de rejoindre le salon" #: src/plugins/plugin_xep_0045.py:116 src/plugins/plugin_xep_0045.py:308 msgid "Group chat error" msgstr "Erreur de salon de discussion" #: src/plugins/plugin_xep_0045.py:266 #, fuzzy msgid "Can't find a MUC service" msgstr "Impossible de trouver la VCard de %s" #: src/plugins/plugin_xep_0045.py:281 #, python-format msgid "%(profile)s is already in room %(room_jid)s" msgstr "%(profile)s est déjà dans le salon %(room_jid)s" #: src/plugins/plugin_xep_0045.py:283 #, python-format msgid "[%(profile)s] is joining room %(room)s with nick %(nick)s" msgstr "[%(profile)s] rejoint %(room)s avec %(nick)s" #: src/plugins/plugin_xep_0045.py:306 #, python-format msgid "Invalid room jid: %s" msgstr "" #: src/plugins/plugin_xep_0045.py:397 #, fuzzy, python-format msgid "user %(nick)s has joined room (%(room_id)s)" msgstr "L'utilisateur %(nick)s a rejoint le salon (%(room_id)s)" #: src/plugins/plugin_xep_0045.py:409 #, python-format msgid "Room [%(room)s] left (%(profile)s))" msgstr "" #: src/plugins/plugin_xep_0045.py:415 #, fuzzy, python-format msgid "user %(nick)s left room (%(room_id)s)" msgstr "L'utilisateur %(nick)s a quitté le salon (%(room_id)s)" #: src/plugins/plugin_xep_0045.py:427 #, fuzzy, python-format msgid "New subject for room (%(room_id)s): %(subject)s" msgstr "Nouveau sujet pour le salon (%(room_id)s): %(subject)s" #: src/plugins/plugin_xep_0047.py:56 #, fuzzy msgid "Implementation of In-Band Bytestreams" msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" #: src/plugins/plugin_xep_0047.py:64 #, fuzzy msgid "In-Band Bytestreams plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_xep_0047.py:79 #, python-format msgid "In-Band Bytestream: TimeOut reached for id %(sid)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0047.py:89 src/plugins/plugin_xep_0065.py:517 #, fuzzy msgid "Client no more in cache" msgstr "fichier [%s] déjà en cache" #: src/plugins/plugin_xep_0047.py:92 src/plugins/plugin_xep_0065.py:520 #, fuzzy msgid "kill id called on a non existant id" msgstr "Demande d'un contact pour un profile inexistant" #: src/plugins/plugin_xep_0047.py:148 msgid "IBB stream opening" msgstr "" #: src/plugins/plugin_xep_0047.py:158 #, python-format msgid "malformed IBB transfer: %s" msgstr "" #: src/plugins/plugin_xep_0047.py:162 #, python-format msgid "Ignoring unexpected IBB transfer: %s" msgstr "" #: src/plugins/plugin_xep_0047.py:166 src/plugins/plugin_xep_0047.py:242 msgid "sended jid inconsistency (man in the middle attack attempt ?)" msgstr "" #: src/plugins/plugin_xep_0047.py:195 msgid "IBB stream closing" msgstr "" #: src/plugins/plugin_xep_0047.py:235 #, fuzzy msgid "Received data for an unknown session id" msgstr "Confirmation inconnue reçue" #: src/plugins/plugin_xep_0047.py:249 msgid "Sequence error" msgstr "" #: src/plugins/plugin_xep_0047.py:262 msgid "Invalid base64 data" msgstr "" #: src/plugins/plugin_xep_0047.py:295 src/plugins/plugin_xep_0065.py:561 msgid "stream length not managed yet" msgstr "" #: src/plugins/plugin_xep_0047.py:324 src/plugins/plugin_xep_0065.py:604 #, fuzzy msgid "Transfer failed" msgstr "Transfert [%s] refusé" #: src/plugins/plugin_xep_0050.py:56 msgid "Left" msgstr "" #: src/plugins/plugin_xep_0050.py:66 #, fuzzy msgid "Implementation of Ad-Hoc Commands" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_xep_0050.py:108 #, fuzzy, python-format msgid "The groups [%(group)s] is unknown for profile [%(profile)s])" msgstr "Tentative d'accès à un profile inconnu" #: src/plugins/plugin_xep_0050.py:210 #, fuzzy msgid "plugin XEP-0050 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0050.py:218 src/plugins/plugin_xep_0100.py:65 msgid "Service" msgstr "" #: src/plugins/plugin_xep_0050.py:218 msgid "commands" msgstr "" #: src/plugins/plugin_xep_0050.py:218 msgid "Execute ad-hoc commands" msgstr "" #: src/plugins/plugin_xep_0050.py:224 msgid "Status" msgstr "" #: src/plugins/plugin_xep_0050.py:237 #, fuzzy msgid "Please select a command" msgstr "Veuillez entrer le nom du nouveau profile" #: src/plugins/plugin_xep_0050.py:334 #, fuzzy msgid "Please enter target jid" msgstr "Veuillez entrer le JID de votre nouveau contact" #: src/plugins/plugin_xep_0050.py:348 #, fuzzy msgid "status selection" msgstr "Sélection du contrat" #: src/plugins/plugin_xep_0050.py:376 msgid "Status updated" msgstr "" #: src/plugins/plugin_xep_0054.py:65 msgid "Implementation of vcard-temp" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_xep_0054.py:75 msgid "Plugin XEP_0054 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0054.py:144 #, python-format msgid "Photo of type [%s] found" msgstr "Photo du type [%s] trouvée" #: src/plugins/plugin_xep_0054.py:146 msgid "Decoding binary" msgstr "Décodage des données" #: src/plugins/plugin_xep_0054.py:153 #, fuzzy, python-format msgid "file saved to %s" msgstr "fichier enregistré dans %s" #: src/plugins/plugin_xep_0054.py:155 #, python-format msgid "file [%s] already in cache" msgstr "fichier [%s] déjà en cache" #: src/plugins/plugin_xep_0054.py:161 msgid "parsing vcard" msgstr "Analyse de la vcard" #: src/plugins/plugin_xep_0054.py:183 #, python-format msgid "FIXME: [%s] VCard tag is not managed yet" msgstr "CORRIGEZ-MOI: la balise VCard [%s] VCard n'est pas encore gérée" #: src/plugins/plugin_xep_0054.py:189 msgid "VCard found" msgstr "VCard trouvée" #: src/plugins/plugin_xep_0054.py:200 msgid "FIXME: vCard not found as first child element" msgstr "CORRIGEZ-MOI: la vCard n'est pas le premier élément enfant" #: src/plugins/plugin_xep_0054.py:206 #, python-format msgid "Can't find VCard of %s" msgstr "Impossible de trouver la VCard de %s" #: src/plugins/plugin_xep_0054.py:215 #, fuzzy msgid "Asking vcard for a non-existant or not connected profile" msgstr "Demande de vcard pour un profile inexistant ou non connecté" #: src/plugins/plugin_xep_0054.py:219 #, python-format msgid "Asking for %s's VCard" msgstr "Demande de la VCard de %s" #: src/plugins/plugin_xep_0054.py:234 #, python-format msgid "Asking for an uncached avatar [%s]" msgstr "Demande d'un avatar qui n'est pas en cache [%s]" #: src/plugins/plugin_xep_0054.py:263 #, fuzzy msgid "Trying to set avatar for a non-existant or not connected profile" msgstr "Demande de vcard pour un profile inexistant ou non connecté" #: src/plugins/plugin_xep_0054.py:308 msgid "New avatar found, requesting vcard" msgstr "Nouvel avatar trouvé, demande de vcard" #: src/plugins/plugin_xep_0055.py:38 #, fuzzy msgid "Implementation of Jabber Search" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_xep_0055.py:45 #, fuzzy msgid "Jabber search plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_xep_0055.py:56 src/plugins/plugin_xep_0055.py:76 msgid "Search directory" msgstr "" #: src/plugins/plugin_xep_0055.py:56 msgid "Search use directory" msgstr "" #: src/plugins/plugin_xep_0055.py:77 #, fuzzy msgid "Please enter the search jid" msgstr "Veuillez entrer le nom du nouveau profile" #: src/plugins/plugin_xep_0055.py:107 src/plugins/plugin_xep_0055.py:165 msgid "No query element found" msgstr "" #: src/plugins/plugin_xep_0055.py:112 src/plugins/plugin_xep_0055.py:170 msgid "No data form found" msgstr "Aucune donnée trouvée" #: src/plugins/plugin_xep_0055.py:119 #, fuzzy, python-format msgid "Fields request failure: %s" msgstr "Échec de l'inscription: %s" #: src/plugins/plugin_xep_0055.py:176 #, fuzzy, python-format msgid "Search request failure: %s" msgstr "Échec de la désinscription: %s" #: src/plugins/plugin_xep_0060.py:34 #, fuzzy msgid "Implementation of PubSub Protocol" msgstr "Implémentation du protocole de transports" #: src/plugins/plugin_xep_0060.py:51 #, fuzzy msgid "PubSub plugin initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0060.py:85 #, fuzzy, python-format msgid "Trying to %(action)s with an unknown profile key [%(profile_key)s]" msgstr "Tentative d'accès à un profile inconnu" #: src/plugins/plugin_xep_0060.py:93 #, fuzzy msgid "INTERNAL ERROR: no handler for required profile" msgstr "ERREUR INTERNE: paramètres xml non valides" #: src/plugins/plugin_xep_0060.py:170 msgid "Publish node deleted" msgstr "" #: src/plugins/plugin_xep_0065.py:90 msgid "Implementation of SOCKS5 Bytestreams" msgstr "Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)" #: src/plugins/plugin_xep_0065.py:142 msgid "Protocol init" msgstr "Initialisation du protocole" #: src/plugins/plugin_xep_0065.py:338 msgid "File transfer completed, closing connection" msgstr "Transfert de fichier terminé, fermeture de la connexion" #: src/plugins/plugin_xep_0065.py:404 msgid "Socks 5 server connection started" msgstr "Connexion du serveur SOCKS 5 démarrée" #: src/plugins/plugin_xep_0065.py:407 #, python-format msgid "Socks 5 server connection lost (reason: %s)" msgstr "Connexion du serveur SOCKS5 perdue (raison: %s)" #: src/plugins/plugin_xep_0065.py:432 msgid "Socks 5 client connection started" msgstr "Connexion du client SOCKS 5 démarrée" #: src/plugins/plugin_xep_0065.py:435 #, python-format msgid "Socks 5 client connection lost (reason: %s)" msgstr "Connexion du client SOCKS5 perdue (raison: %s)" #: src/plugins/plugin_xep_0065.py:462 msgid "Plugin XEP_0065 initialization" msgstr "Initialisation du plugin XEP_0065" #: src/plugins/plugin_xep_0065.py:468 #, fuzzy msgid "registering" msgstr "enregistrement" #: src/plugins/plugin_xep_0065.py:476 #, python-format msgid "Launching Socks5 Stream server on port %d" msgstr "Lancement du serveur de flux Socks5 sur le port %d" #: src/plugins/plugin_xep_0065.py:507 #, python-format msgid "Socks5 Bytestream: TimeOut reached for id %(sid)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0065.py:557 msgid "Unknown profile, this should not happen" msgstr "" #: src/plugins/plugin_xep_0065.py:614 #, fuzzy msgid "Internal error, can't do transfer" msgstr "ERREUR INTERNE: état de jeu inconnu" #: src/plugins/plugin_xep_0065.py:624 src/plugins/plugin_xep_0065.py:797 msgid "No streamhost found in stream query" msgstr "" #: src/plugins/plugin_xep_0065.py:629 msgid "A proxy server is used" msgstr "" #: src/plugins/plugin_xep_0065.py:634 msgid "Proxy jid is not the same as in parameters, this should not happen" msgstr "" #: src/plugins/plugin_xep_0065.py:642 src/plugins/plugin_xep_0065.py:736 #, fuzzy msgid "activating stream" msgstr "Lancement du flux" #: src/plugins/plugin_xep_0065.py:660 #, fuzzy msgid "Can't activate the proxy stream" msgstr " Ferme l'application" #: src/plugins/plugin_xep_0065.py:690 msgid "BS stream query" msgstr "" #: src/plugins/plugin_xep_0065.py:704 #, python-format msgid "Ignoring unexpected BS transfer: %s" msgstr "" #: src/plugins/plugin_xep_0065.py:713 #, python-format msgid "No streamhost found in stream query %s" msgstr "" #: src/plugins/plugin_xep_0065.py:722 msgid "incomplete streamhost element" msgstr "" #: src/plugins/plugin_xep_0065.py:728 #, fuzzy, python-format msgid "Stream proposed: host=[%(host)s] port=[%(port)s]" msgstr "Flux proposé: serveur=[%(host)s] port=[%(post)s]" #: src/plugins/plugin_xep_0065.py:789 msgid "Can't determine proxy information" msgstr "" #: src/plugins/plugin_xep_0065.py:793 msgid "Bad answer received from proxy" msgstr "" #: src/plugins/plugin_xep_0065.py:800 msgid "" "Multiple streamhost elements in proxy not managed, keeping only the first one" msgstr "" #: src/plugins/plugin_xep_0065.py:813 msgid "No proxy found on this server" msgstr "" #: src/plugins/plugin_xep_0071.py:44 #, fuzzy msgid "Implementation of XHTML-IM" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_xep_0071.py:72 #, fuzzy msgid "XHTML-IM plugin initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0071.py:114 src/plugins/plugin_xep_0277.py:259 msgid "Can't have xhtml and rich content at the same time" msgstr "" #: src/plugins/plugin_xep_0077.py:39 msgid "Implementation of in-band registration" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0077.py:46 msgid "Plugin XEP_0077 initialization" msgstr "Initialisation du plugin XEP_0077" #: src/plugins/plugin_xep_0077.py:64 #, fuzzy msgid "Can't find data form" msgstr "Impossible de trouver la VCard de %s" #: src/plugins/plugin_xep_0077.py:65 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é :(" #: src/plugins/plugin_xep_0077.py:109 #, python-format msgid "Asking registration for [%s]" msgstr "Demande d'enregistrement pour [%s]" #: src/plugins/plugin_xep_0085.py:49 #, fuzzy msgid "Implementation of Chat State Notifications Protocol" msgstr "Implémentation du protocole de transports" #: src/plugins/plugin_xep_0085.py:89 msgid "Enable chat state notifications" msgstr "" #: src/plugins/plugin_xep_0085.py:93 #, fuzzy msgid "Chat State Notifications plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_xep_0095.py:51 #, fuzzy msgid "Implementation of Stream Initiation" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0095.py:58 #, fuzzy msgid "Plugin XEP_0095 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0095.py:75 #, fuzzy msgid "XEP-0095 Stream initiation" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0095.py:161 msgid "sending stream initiation accept answer" msgstr "" #: src/plugins/plugin_xep_0095.py:183 msgid "Asking for an non-existant or not connected profile" msgstr "Demande d'un profile inexistant ou non connecté" #: src/plugins/plugin_xep_0095.py:188 #, python-format msgid "Stream Session ID: %s" msgstr "" #: src/plugins/plugin_xep_0096.py:43 msgid "Implementation of SI File Transfer" msgstr "" "Implémentation de l'initialisation de flux pour le transfert de fichier " #: src/plugins/plugin_xep_0096.py:50 msgid "Plugin XEP_0096 initialization" msgstr "Initialisation du plugin XEP_0096" #: src/plugins/plugin_xep_0096.py:65 #, python-format msgid "SI File Transfer: TimeOut reached for id %s" msgstr "" #: src/plugins/plugin_xep_0096.py:80 msgid "XEP-0096 file transfer requested" msgstr "" #: src/plugins/plugin_xep_0096.py:100 #, python-format msgid "File proposed: name=[%(name)s] size=%(size)s" msgstr "Fichier proposé: nom=[%(name)s] taille=%(size)s" #: src/plugins/plugin_xep_0096.py:107 #, fuzzy msgid "No file element found" msgstr "Aucun profile sélectionné" #: src/plugins/plugin_xep_0096.py:208 #, python-format msgid "Transfer %(id)s failed with stream method %(s_method)s: %(reason)s" msgstr "" #: src/plugins/plugin_xep_0096.py:223 #, fuzzy, python-format msgid "File transfer refused by %s" msgstr "Transfert [%s] refusé" #: src/plugins/plugin_xep_0096.py:224 #, fuzzy, python-format msgid "The contact %s refused your file" msgstr "Le contact %s a refusé votre inscription" #: src/plugins/plugin_xep_0096.py:224 #, fuzzy msgid "File refused" msgstr "refusé" #: src/plugins/plugin_xep_0096.py:226 #, python-format msgid "Error during file transfer with %s" msgstr "" #: src/plugins/plugin_xep_0096.py:227 #, python-format msgid "" "Something went wrong during the file transfer session intialisation with %s" msgstr "" #: src/plugins/plugin_xep_0096.py:227 #, fuzzy msgid "File transfer error" msgstr "Transfert de fichier" #: src/plugins/plugin_xep_0096.py:299 #, python-format msgid "Transfer %(sid)s successfuly finished [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0096.py:305 #, python-format msgid "" "Transfer %(id)s failed with stream method %(s_method)s: %(reason)s " "[%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0100.py:35 msgid "Implementation of Gateways protocol" msgstr "Implémentation du protocole de transports" #: src/plugins/plugin_xep_0100.py:38 #, 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)." #: src/plugins/plugin_xep_0100.py:43 msgid "Internet Relay Chat" msgstr "" #: src/plugins/plugin_xep_0100.py:44 msgid "XMPP" msgstr "" #: src/plugins/plugin_xep_0100.py:45 msgid "Tencent QQ" msgstr "" #: src/plugins/plugin_xep_0100.py:46 msgid "SIP/SIMPLE" msgstr "" #: src/plugins/plugin_xep_0100.py:47 msgid "ICQ" msgstr "" #: src/plugins/plugin_xep_0100.py:48 msgid "Yahoo! Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:49 msgid "Gadu-Gadu" msgstr "" #: src/plugins/plugin_xep_0100.py:50 msgid "AOL Instant Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:51 msgid "Windows Live Messenger" msgstr "" #: src/plugins/plugin_xep_0100.py:58 msgid "Gateways plugin initialization" msgstr "Initialisation de l'extension pour les transports" #: src/plugins/plugin_xep_0100.py:65 #, fuzzy msgid "gateways" msgstr "Chercher les transports" #: src/plugins/plugin_xep_0100.py:65 #, fuzzy msgid "Find gateways" msgstr "Chercher les transports" #: src/plugins/plugin_xep_0100.py:77 msgid "Invalid JID" msgstr "" #: src/plugins/plugin_xep_0100.py:84 #, fuzzy, python-format msgid "Gateways manager (%s)" msgstr "Gestionnaire de transport" #: src/plugins/plugin_xep_0100.py:92 #, python-format msgid "Failed (%s)" msgstr "" #: src/plugins/plugin_xep_0100.py:105 #, fuzzy msgid "Use external XMPP server" msgstr "Utiliser un autre serveur XMPP:" #: src/plugins/plugin_xep_0100.py:107 msgid "Go !" msgstr "" #: src/plugins/plugin_xep_0100.py:114 #, fuzzy msgid "No gateway index selected" msgstr "Aucun profile sélectionné" #: src/plugins/plugin_xep_0100.py:128 #, python-format msgid "" "INTERNAL ERROR: identity category should always be \"gateway\" in " "_getTypeString, got \"%s\"" msgstr "" #: src/plugins/plugin_xep_0100.py:132 msgid "Unknown IM" msgstr "Messagerie inconnue" #: src/plugins/plugin_xep_0100.py:136 msgid "Registration successful, doing the rest" msgstr "Inscription réussie, lancement du reste de la procédure" #: src/plugins/plugin_xep_0100.py:159 msgid "Timeout" msgstr "" #: src/plugins/plugin_xep_0100.py:170 #, fuzzy, python-format msgid "Found gateway [%(jid)s]: %(identity_name)s" msgstr "Transport trouvé (%(jid)s): %(identity)s" #: src/plugins/plugin_xep_0100.py:173 #, python-format msgid "Skipping [%(jid)s] which is not a gateway" msgstr "" #: src/plugins/plugin_xep_0100.py:180 msgid "No gateway found" msgstr "Aucun transport trouvé" #: src/plugins/plugin_xep_0100.py:185 #, python-format msgid "item found: %s" msgstr "object trouvé: %s" #: src/plugins/plugin_xep_0100.py:209 #, fuzzy, python-format msgid "find gateways (target = %(target)s, profile = %(profile)s)" msgstr "transports trouvée (cible = %s)" #: src/plugins/plugin_xep_0115.py:54 #, fuzzy msgid "Implementation of entity capabilities" msgstr "Implementation de vcard-temp" #: src/plugins/plugin_xep_0115.py:82 #, fuzzy msgid "Plugin XEP_0115 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0115.py:148 #, python-format msgid "Capability hash generated: [%s]" msgstr "" #: src/plugins/plugin_xep_0163.py:42 #, fuzzy msgid "Implementation of Personal Eventing Protocol" msgstr "Implémentation du protocole de transports" #: src/plugins/plugin_xep_0163.py:49 #, fuzzy msgid "PEP plugin initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0163.py:97 #, fuzzy, python-format msgid "Trying to send personal event with an unknown profile key [%s]" msgstr "Tentative d'appel d'un profile inconnue" #: src/plugins/plugin_xep_0163.py:100 #, fuzzy msgid "Trying to send personal event for an unknown type" msgstr "Tentative d'assigner un paramètre à un profile inconnu" #: src/plugins/plugin_xep_0163.py:106 #, fuzzy msgid "No item found" msgstr "Aucun transport trouvé" #: src/plugins/plugin_xep_0163.py:111 msgid "Can't find mood element in mood event" msgstr "" #: src/plugins/plugin_xep_0163.py:115 #, fuzzy msgid "No mood found" msgstr "Aucune donnée trouvée" #: src/plugins/plugin_xep_0249.py:51 #, fuzzy msgid "Implementation of Direct MUC Invitations" msgstr "Implémentation de l'enregistrement en ligne" #: src/plugins/plugin_xep_0249.py:71 msgid "Auto-join MUC on invitation" msgstr "" #: src/plugins/plugin_xep_0249.py:77 #, fuzzy msgid "Plugin XEP_0249 initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0249.py:95 src/plugins/plugin_xep_0249.py:123 #, fuzzy msgid "Profile doesn't exists !" msgstr "Le fichier [%s] n'existe pas !" #: src/plugins/plugin_xep_0249.py:125 #, python-format msgid "Invitation accepted for room %(room)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0249.py:137 #, python-format msgid "Invitation received for room %(room)s [%(profile)s]" msgstr "" #: src/plugins/plugin_xep_0249.py:139 msgid "Error while parsing invitation" msgstr "" #: src/plugins/plugin_xep_0249.py:143 msgid "Invitation silently discarded because user is already in the room." msgstr "" #: src/plugins/plugin_xep_0249.py:154 #, python-format msgid "" "You have been invited by %(user)s to join the room %(room)s. Do you accept?" msgstr "" #: src/plugins/plugin_xep_0249.py:154 src/plugins/plugin_xep_0249.py:157 #, fuzzy msgid "MUC invitation" msgstr "Connexion..." #: src/plugins/plugin_xep_0249.py:157 #, python-format msgid "" "An invitation from %(user)s to join the room %(room)s has been declined " "according to your personal settings." msgstr "" #: src/plugins/plugin_xep_0277.py:47 #, fuzzy msgid "Implementation of microblogging Protocol" msgstr "Implémentation du protocole de transports" #: src/plugins/plugin_xep_0277.py:58 #, fuzzy msgid "Microblogging plugin initialization" msgstr "Initialisation du plugin XEP_0054" #: src/plugins/plugin_xep_0277.py:132 #, python-format msgid "No entry found in the pubsub item %s" msgstr "" #: src/plugins/plugin_xep_0277.py:155 #, python-format msgid "Atom entry %s misses a required element" msgstr "" #: src/plugins/plugin_xep_0277.py:181 #, python-format msgid "Can't parse the link element of pubsub entry %s" msgstr "" #: src/plugins/plugin_xep_0277.py:190 #, python-format msgid "Can't find author element in pubsub entry %s" msgstr "" #: src/plugins/plugin_xep_0277.py:218 msgid "Content of type XHTML must declare its namespace!" msgstr "" #: src/plugins/plugin_xep_0277.py:302 msgid "Microblog data must contain at least 'content' key" msgstr "" #: src/plugins/plugin_xep_0277.py:306 msgid "Microblog data's content value must not be empty" msgstr "" #: src/plugins/plugin_xep_0277.py:340 #, fuzzy msgid "Can't find profile's jid" msgstr "Impossible de trouver la VCard de %s" #: src/plugins/plugin_xep_0277.py:347 #, python-format msgid "Microblog node has now access %s" msgstr "" #: src/plugins/plugin_xep_0277.py:351 msgid "Can't set microblog access" msgstr "" #: src/test/constants.py:43 msgid "Enable unibox" msgstr "" #: src/test/constants.py:44 msgid "'Wysiwyg' edition" msgstr "" #: src/test/test_plugin_misc_room_game.py:43 msgid "Dummy plugin to test room game" msgstr "" #: src/tools/misc.py:58 #, python-format msgid "There is already a bound priority [%s]" msgstr "" #: src/tools/misc.py:60 #, python-format msgid "There is already a trigger with the same priority [%s]" msgstr "" #: src/tools/xml_tools.py:177 #, fuzzy msgid "INTERNAL ERROR: parameters xml not valid" msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" #: src/tools/xml_tools.py:186 msgid "INTERNAL ERROR: params categories must have a name" msgstr "ERREUR INTERNE: les catégories des paramètres doivent avoir un nom" #: src/tools/xml_tools.py:194 msgid "INTERNAL ERROR: params must have a name" msgstr "ERREUR INTERNE: les paramètres doivent avoir un nom" #: src/tools/xml_tools.py:276 msgid "TabElement must be a child of TabsContainer" msgstr "" #: src/tools/xml_tools.py:316 msgid "Can't set row index if auto_index is True" msgstr "" #: src/tools/xml_tools.py:416 msgid "either items or columns need do be filled" msgstr "" #: src/tools/xml_tools.py:429 msgid "Headers lenght doesn't correspond to columns" msgstr "" #: src/tools/xml_tools.py:475 msgid "Incorrect number of items in list" msgstr "" #: src/tools/xml_tools.py:578 msgid "Value must be 0, 1, false or true" msgstr "" #: src/tools/xml_tools.py:612 msgid "empty \"options\" list" msgstr "" #: src/tools/xml_tools.py:614 msgid "invalid styles" msgstr "" #: src/tools/xml_tools.py:652 #, fuzzy, python-format msgid "Unknown panel type [%s]" msgstr "Type d'action inconnu" #: src/tools/xml_tools.py:654 msgid "form XMLUI need a submit_id" msgstr "" #: src/tools/xml_tools.py:656 msgid "container argument must be a string" msgstr "" #: src/tools/xml_tools.py:748 #, fuzzy, python-format msgid "Unknown container type [%s]" msgstr "Type d'action inconnu" #: src/tools/xml_tools.py:767 #, fuzzy, python-format msgid "Invalid type [%s]" msgstr "Type d'action inconnu" #~ msgid "" #~ "\n" #~ " %prog [options] [FILE1 FILE2 ...] JID\n" #~ " %prog -w [options] [JID1 JID2 ...]\n" #~ "\n" #~ " %prog --help for options list\n" #~ " " #~ msgstr "" #~ "\n" #~ " %prog [options] [FICHIER1 FICHIER2 ...] JID\n" #~ " %prog -w [options] [JID1 JID2 ...]\n" #~ "\n" #~ " %prog --help pour la liste des options\n" #~ " " #~ msgid "You must specify the destination JID (Jabber ID)" #~ msgstr "Vous devez préciser le JID (Jabber ID) de destination" #~ msgid "Option progress is not available, deactivated." #~ msgstr "" #~ "L'option « progress » (barre de progression) n'est pas disponible, elle " #~ "est désactivée." #~ msgid "OK !" #~ msgstr "C'est parti !" #~ msgid "FIXME: actionResult not implemented" #~ msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" #~ msgid "GO !" #~ msgstr "C'est parti !" #, fuzzy #~ msgid "You must enter an external server JID" #~ msgstr "Utiliser un autre serveur XMPP:" #, fuzzy #~ msgid "Register" #~ msgstr "enregistrement" #, fuzzy #~ msgid "Unregister" #~ msgstr "enregistrement" #, fuzzy #~ msgid "Find gateways request" #~ msgstr "Demande de recherche de transports" #~ msgid "Unmanaged tag" #~ msgstr "Tab inconnu" #~ msgid "text node has no child !" #~ msgstr "le nœud text n'a pas d'enfant !" #, fuzzy #~ msgid "INTERNAL ERROR: Unmanaged show_type (%s)" #~ msgstr "ERREUR INTERNE: contrat inconnu (mal orthographié ?)" #, fuzzy #~ msgid "Opening gateways manager on [%s]" #~ msgstr "Envoi du message jabber à %s" #~ msgid "&Find Gateways" #~ msgstr "Trouver transports" #~ msgid " Find gateways to legacy IM" #~ msgstr " Trouve les transports vers les messageries externes" #~ msgid "Find Gateways request" #~ msgstr "Demande de recherche de transports" #, fuzzy #~ msgid "Parameters error" #~ msgstr "&Paramètres" #~ msgid "FIXME FIXME FIXME" #~ msgstr "CORRIGER-MOI" #~ msgid "Submitting form" #~ msgstr "Envoi du formulaire" #~ msgid "No user or server given" #~ msgstr "L'utilisateur ou le serveur n'ont pas été spécifié" #~ msgid "FIXME FIXME FIXME: Unmanaged action (%s) in submitForm" #~ msgstr "" #~ "CORRIGEZ-MOI CORRIGEZ-MOI CORRIGEZ-MOI: Action non gérée (%s) dans " #~ "\"submitForm\"" #~ msgid "Your are now unregistred" #~ msgstr "Vous êtes maintenant désinscrit" #~ msgid "Unregistration failure: %s" #~ msgstr "Échec de la désinscription: %s" #~ msgid "Unregistration failed: %s" #~ msgstr "Échec de la désinscription: %s" #, fuzzy #~ msgid "Registration failed" #~ msgstr "Éched de l'insciption (%s)" #~ msgid "All items checked for id [%s]" #~ msgstr "Tous les points ont été vérifiés pour l'id [%s]" #, fuzzy #~ msgid "Error when discovering [%(jid)s]: %(error)s" #~ msgstr "Erreur en analysant [%(jid)s]: %(condition)s" #, fuzzy #~ msgid "Error when discovering [%(target)s]: %(condition)s" #~ msgstr "Erreur en analysant [%(jid)s]: %(condition)s" #~ msgid "Error while trying to discover %(target)s gateways: %(error_mess)s" #~ msgstr "Erreur en essayant d'analyser %(target)s portails: %(error_mess)s" #, fuzzy #~ msgid "Unknown layout type [%s]" #~ msgstr "Type d'action inconnu" #~ msgid "Trying to add a category without parent tabs layout" #~ msgstr "" #~ "Tentative d'ajout d'une catégorie sans disposition dans l'onglet parent" #~ msgid "parent layout of a category is not tabs" #~ msgstr "la disposition parente d'une catégorie n'est pas \"tabs\" (onglets)" #~ msgid "WARNING: unknown text type" #~ msgstr "ATTENTION: type de texte inconnu" #~ msgid "Ok" #~ msgstr "Ok" #~ msgid "Yes" #~ msgstr "Oui" #~ msgid "No" #~ msgstr "Non" #, fuzzy #~ msgid "INTERNAL ERROR: Tab not found" #~ msgstr "ERREUR INTERNE: paramètres xml non valides" #~ msgid "Impossible to list directory" #~ msgstr "Impossible de lister les répertoires" #~ msgid "Path: " #~ msgstr "Chemin:" #~ msgid "Bookmarks" #~ msgstr "Favoris" #~ msgid "No GTK bookmarks file found" #~ msgstr "Aucun fichier favori pour GTK trouvé" #~ msgid "No KDE bookmarks file found" #~ msgstr "Aucun fichier favori pour KDE trouvé" #~ msgid "Contact List" #~ msgstr "Liste de contacts" #~ msgid "Incomplete data" #~ msgstr "Données incomplétes" #, fuzzy #~ msgid "Trying to call unknown function (%s)" #~ msgstr "Tentative d'appel d'une fonction inconnue" #, fuzzy #~ msgid "Trying to access an unknown menu (%(category)s/%(name)s/%(type)s)" #~ msgstr "Demande d'un paramètre inconnu: (%(category)s/%(name)s)" #~ msgid "general params data loaded" #~ msgstr "Paramètres généraux chargés" #~ msgid "individual params data loaded" #~ msgstr "Paramètres individuels chargés" #~ msgid "params template loaded" #~ msgstr "Modèle des paramètres chargé" #~ msgid "params loaded" #~ msgstr "paramètres chargés" #~ msgid "history loaded" #~ msgstr "Historique chargée" #~ msgid "private values loaded" #~ msgstr "Données privées chargées" #~ msgid "Can't load private values !" #~ msgstr "Impossible de charger les données privées !" #~ msgid "params saved" #~ msgstr "Paramètres sauvés" #~ msgid "history saved" #~ msgstr "Historique sauvée" #~ msgid "private values saved" #~ msgstr "Données privées sauvées" #~ msgid "source JID not found !" #~ msgstr "JID source introuvable !" #~ msgid "dest JID not found !" #~ msgstr "JID destination introuvable !" #~ msgid "Trying to add a contact to a non-existant profile" #~ msgstr "Tentative d'ajout d'un contact à un profile inexistant" #~ msgid "Adding connection: %(address)s, %(connection)s" #~ msgstr "Ajout d'une connexion: %(address)s, %(connection)s" #~ msgid "Saving file in %s." #~ msgstr "Sauvegarde du fichier dans %s." #~ msgid "Launching socks5 initiator" #~ msgstr "Lancement de socks5 en mode initiateur" #~ msgid "XEP-0096 management" #~ msgstr "Gestion de XEP-0096" #~ msgid "Transfer [%s] accepted" #~ msgstr "Transfert [%s] accepté" #~ msgid "Approved unknow id !" #~ msgstr "id inconnue approuvée !" #~ msgid "Feature negociation" #~ msgstr "Négociation de fonctionnalités" #~ msgid "presence update for [%s]" #~ msgstr "mise à jour de l'information de présence pour [%s]" #, fuzzy #~ msgid "hiding %s" #~ msgstr "Ajout de %s" #, fuzzy #~ msgid "showing %s" #~ msgstr "Ajout de %s" #, fuzzy #~ msgid "FIXME: askConfirmation not implemented" #~ msgstr "CORRIGEZ-MOI: actionResult n'est pas implémenté" sat-0.6.1.1+hg20180208/i18n/fr/0002755000175500017600000000000013243470024015162 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/i18n/fr/LC_MESSAGES/0002755000175500017600000000000013243470024016747 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/i18n/fr/LC_MESSAGES/sat.mo0000644000175500017600000006075413243470024020105 0ustar debaclelocal_srcD]lV58 nx+X+G's+  !( 1 <HX$i'#4, GM 'F:K*3"$C7`;K;FE0CYm!(& -Gu  ,+ $A F T^. 3 9 F Q p x 9 Y 2!'P!4x!#!"!$!&"@"a]"$" ")"##3#G# O#\#e#j#z#"~###'## #$>$W$ ^$l$ r$|$$$Z$8$8%S%!p%"%%%%%&8&A& ^&Xi& &T&%':'S''l''''E'.(7K(((( (O()/)5A)+w)!)+)!)$*8*N*V*]*e*K*-G+,u+C+@+-',U,Ft,2,/,Q-p-- -&-#-7-(0.,Y. .$....3 / @/'L/t/$|/////]0"a0 0?0B0 19 1;Z1<1=1C2<U22?232E3U3,[333+333 44O*4z444424 55E(5n5555 55595:6N6-f6 6666 66:7^?838 8"8919O9Yd929/91!:S:p:x:: ::: :: ::;$;'A;1i; ;; ;I;8 <JB<< <<'<6<X&=D=.==0 >/>>.n>">>>D>D?K?R?i?K{?M?X@$n@@@#@%@@&A?A&PAwAAA A(AAAABAB3_B<BBBC0C8GC=C,C CC DDE E E E1)E [EeEJEiED8F4}F8F*FIGB`G,GGG$yHH2H H H*IJJJJ JJ JJdK@{KK/K) L75L mL!wL!L!L!LL%M .MfVV# W%.W%TW'zW@WW0W#X(,XUXgX X&XtX)@YjYTyYTY #Z,0Zb]ZjZj+[t[b \n\?w\@\g\ `],j]]]/]]^)^<^[L^^^"^^@_U_i_a___(`.`I`d`k`;s`P`(a1)a[a saa$aaa x_Pi+yJ41F-l'KNq[0 he:<3DC!us*6L.G W7UaV#SMY H{T$`tZ Qb\"Bok=;E/%(~R8pXn?z d5|I}v@)]f,jOrc2^g>mw&9A -- %(player)s: score for this game ==> %(score_game)i total score ==> %(total_score)i subject: %(subject)s message: --- %(message)s --- About %s Add a contact to your list Configure the program Remove the selected contact from your list Terminate the program%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is waiting for one%(name)s is a SàT (Salut à Toi) frontend %(player)s win the bid with %(contrat)s%(profile)s is already in room %(room_jid)s%s is not a valid JID !&Action&Add contact&Connect CTRL-c&Contacts&Disconnect CTRL-d&Games&General&Join Room&Parameters&Remove contact&SendFile CTRL-s********** [%s] CONNECTED ******************** [%s] DISCONNECTED **********A profile name can't start with a @A&boutAFKAboutAccept multiple files (you'll have to stop manually)Accepted file [%(filename)s] from %(sender)sAdd a new line at the beginning of the input (usefull for ascii art ;))Add contact requestAdding %sAdding a contactAdding users %s to roomAre you sure to delete the profile [%s]Are you sure to register new account [%(user)s] to server %(server)s ?Are you sure you want to delete %s from your roster list ?Asking contacts for a non-existant profileAsking for %s's VCardAsking for an non-existant or not connected profileAsking for an uncached avatar [%s]Asking params for inexistant profileAsking registration for [%s]Asking waiting subscriptions for a non-existant profileAttempt to register two callbacks for the same confirmationAvatarAwayBad profile nameBirthdayCS friend found: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)Can't connect to SàT backend, are you sure it's launched ?Can't determine default value for [%(category)s/%(name)s]: %(reason)sCan't find VCard of %sCancelCancelling formCards played are invalid !Cards played are not valid: %sChanging JID to %sChoose a file to sendClear progress listContact List initContact suppressionDNDDecoding binaryDeleteDo you put these cards in chien ?E&xitE-mailEntering a MUC roomErrorFIXME: [%s] VCard tag is not managed yetFIXME: temporary menu, must be changedFIXME: vCard not found as first child elementFeature found: %sFile RequestFile TransferFile proposed: name=[%(name)s] size=%(size)sFile transfer completed, closing connectionFile transfer confirmation askedForce overwritting of existing filesFormFree for chatFull NameG'day %(name)s, you have %(nb_message)i unread message%(plural_mess)s and %(unread_CR_mess)s unread couch request message%(plural_CR)s If you want to send a message, select the recipient(s) in the list belowGameGardeGarde ContreGarde SansGateways plugin initializationGeneralGroup chat errorINTERNAL ERROR: Unexpected class for main widget's footerINTERNAL ERROR: no confirmation of message sent by CS, maybe the site has been modified ?INTERNAL ERROR: params categories must have a nameINTERNAL ERROR: params must have a nameIdentity found: [%(category)s/%(type)s] %(identity)sImplementation of Gateways protocolImplementation of SI File TransferImplementation of SOCKS5 BytestreamsImplementation of in-band registrationImplementation of vcard-tempImpossible to contact CS website, please check your login/password, connection or try again laterInternal error: unmanaged game stageJoin roomLaunching Socks5 Stream server on port %dLoginMake a bzip2 tarballMemory manager initMessageMessage sentMessagesMiscMy turn to playNewNew avatar found, requesting vcardNicknameNo data form foundNo default profile, returning first oneNo gateway foundNo keep_alifeNo profile selectedNo user, password or server given, can't register new account.OnlineParam requestPassePassword:PetitePhone #Photo of type [%s] foundPlayer %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensationPlayer %(player)s is ready to start [status: %(status)s]Please choose your contratPlease enter new contact JIDPlease enter the new profile namePleeeeasse, I can't even breathe !PluginPlugin XEP_0054 initializationPlugin XEP_0065 initializationPlugin XEP_0077 initializationPlugin XEP_0096 initializationProfile:Progress bar deactivated -- Progress: ProgressBar not available, please download it at http://pypi.python.org/pypi/progressbarProtocol initRefused file [%(filename)s] from %(sender)s: a file with the same name already existRegister new accountRegistration failed (%s)Registration failure: %sRegistration successful, doing the restRegistration successfullRemove contact requestReplacing user %sRequested param [%(name)s] in category [%(category)s] doesn't exist !Requesting a param for an non-existant profileRequesting an unknown parameter (%(category)s/%(name)s)SaveSaving new passwordSelect the profile to useSend FileSeparate xmpp messages: send one message per line instead of one message alone.Show contact's profile requestShow progress barShow unread message%(plural)s in external web browserSocks 5 client connection lost (reason: %s)Socks 5 client connection startedSocks 5 server connection lost (reason: %s)Socks 5 server connection startedStarting compression, please wait...Status change requestSubjectSubmitSuccessThe attacker (%(attaquant)s) makes %(points)i and needs to make %(point_limit)i (%(nb_bouts)s oulder%(plural)s%(separator)s%(bouts)s): he %(victory)sThe contact %(jid)s wants to send you the file %(filename)s Do you accept ?The contact %s has accepted your subscriptionThe contact %s has refused your subscriptionThe contact %s wants to subscribe to your presence. Do you accept ?The form data is not sent back, the type is not managed properlyThe message has been sent to every recipientsThe winner of this trick is %sThere is already one profile plugged (we are in single profile mode) !There is not recipient selected for this message !This gateway can't be managed by SàT, sorry :(This plugin allow to manage your CouchSurfing account throught your SàT frontendThis profile is not pluggedTransfer [%s] refusedTray ClickTrying to access an undefined constantTrying to access an unknown profileTrying to add presence status to a non-existant profileTrying to connect a non-exsitant profileTrying to remove an unknow progress callbackUnknown IMUnknown or disconnected profile (%s)Unmanaged card game element: %sUnsubscribing %s presenceUser interruption: good byeUsername already exists, please choose an other oneVCard foundWait for a file to be sent by a contactWebsiteWhere do you want to save the file ?Wix jabber clientXML stream is initializedXML user interface receivedYes/No confirmation askedYou have to fill your CouchSurfing login & password in parameters before using this interfaceYou haven't selected any contact !You loose :(You must select a profile or create a new one before connectingYou need to be exactly 4 peoples in the room to start a Tarot gameYou win \o/[%(profile)s] is joining room %(room)s with nick %(nick)s[%s] is a dir ! Please send files inside or use compression[INTERNAL] trying to remove user for a non group chat window[INTERNAL] trying to replace user for a non group chat window[INTERNAL] trying to set presents nicks for a non group chat window[INTERNAL] trying to set subject for a non group chat windowacceptedactionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]asking connection status for a non-existant profilebz2 is an experimental option at an early dev stage, use with cautionclosecontrat [%(contrat)s] choosed by %(profile)scontrat selectionfile [%s] already in cachegetGame is not implemented in this frontendgot message from: %simporting plugin: %sitem found: %sname@server.tldnew Tarot game started by [%(referee)s] in room [%(room_jid)s] with %(players)snew contact in roster list: %snot connected !now we print historyonContactActivated: %sparam update: [%(namespace)s] %(name)s = %(value)sparsing vcardplugin profile %spresence update for [%(entity)s] (unavailable, statuses=%(statuses)s)profile %s is unknownrefusedregistration answer: %sremoving %s from roster listrunning appscoressendsending message to %(friends)s with subject [%(subject)s]setting param: %(name)s=%(value)s in category %(category)ssetting plugins parentsstartGame is not implemented in this frontendstopping appsubscription approved for [%s]unknown id, ignoringunsubscription asked for [%s]update %sÉcartProject-Id-Version: 0.0.2 Report-Msgid-Bugs-To: POT-Creation-Date: 2014-02-24 19:12+0100 PO-Revision-Date: 2010-03-05 19:24+1100 Last-Translator: Goffi Language-Team: French Language: fr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- %(player)s: points pour cette partie ==> %(score_game)i point au total ==> %(total_score)i sujet: %(subject)s message: --- %(message)s --- À propos %s Ajouter un contact à votre liste Configurer l'application Supprime le contact sélectionné de votre liste Ferme l'application%(excuse_owner)s garde l'Excuse mais n'a aucune carte à donner, %(winner)s en attend une%(name)s est un frontend pour SàT (Salut à Toi) %(player)s remporte l'enchère avec %(contrat)s%(profile)s est déjà dans le salon %(room_jid)s%s n'est pas un JID valide !&Action&Ajouter un contact&Connexion CTRL-c&Contacts&Déconnexion CTRL-dJeux&GénéralRe&joindre un salon&ParamètresSupp&rimer un contactEnvoi de fichier CTRL-s********** [%s] CONNECTÉ ******************** [%s] DÉCONNECTÉ **********Un nom de profile ne peut pas commencer avec un @À proposLoin du clavierÀ proposAccepte plusieurs fichiers (vous devrez arrêter le programme à la main)Le fichier [%(filename)s] de %(sender)s a été acceptéAjoute un saut de ligne au début de l'entrée (utile pour l'art ascii ;))Demande d'ajout de contactAjout de %sAjout d'un contactAjout de l'utilisateur %s dans le salonÊtes vous sûr de vouloir supprimer le profile [%s] ?Êtes vous sûr de vouloir inscrire le nouveau compte [%(user)s] au serveur %(server)s ?Êtes vous sûr de vouloir supprimer %s de votre liste de contacts ?Demande de contacts pour un profile inexistantDemande de la VCard de %sDemande d'un profile inexistant ou non connectéDemande d'un avatar qui n'est pas en cache [%s]Demande de paramètres pour un profile inconnuDemande d'enregistrement pour [%s]Demande des inscriptions en attente pour un profile inexistantTentative de déclaration de 2 callbacks pour la même configurationAvatarAbsentMauvais nom de profileDate de naissanceAmis CS trouvé: %(friend_name)s (id: %(friend_id)s, link: %(friend_link)s)Impossible de se connecter au démon SàT, êtes vous sûr qu'il est lancé ?Impossible de déterminer la valeur par défaut pour [%(category)s/%(name)s]: %(reason)sImpossible de trouver la VCard de %sAnnulerAnnulation du formulaireLes cartes jouées sont invalides !Les cartes jouées sont invalides: %sChangement du JID pour %sVeuillez choisir le fichier à envoyerEffacer la listeInitialisation de la liste de contactsSuppression de contactNe pas dérangerDécodage des donnéesSuppressionVoulez-vous placer ces cartes au chien ?QuitterCourrielEntrée dans le salon MUCErreurCORRIGEZ-MOI: la balise VCard [%s] VCard n'est pas encore géréeCORRIGEZ-MOI: menu temporaire, doit être remplacéCORRIGEZ-MOI: la vCard n'est pas le premier élément enfantFonctionnalité trouvée: %sGestion de fichiersTransfert de fichierFichier proposé: nom=[%(name)s] taille=%(size)sTransfert de fichier terminé, fermeture de la connexionDemande de confirmation pour un transfer de fichier demandéeForce le remplacement des fichiers existantsFormulaireLibre pour discuterNom completBonjour %(name)s, vous avez %(nb_message)i message%(plural_mess)s non lus et %(unread_CR_mess)s requête%(plural_CR)s d'hébergement en attente Si vous voulez envoyer un message, sélectionnez le(s) destinataire(s) dans la liste ci-dessous.JeuGardeGarde ContreGarde SansInitialisation de l'extension pour les transportsGénéralErreur de salon de discussionERREUR INTERNE: Classe inattendue pour le pied de page du widget principalERREUR INTERNE: aucune confirmation du message envoyée par CS, peut être que le site a été modifié ?ERREUR INTERNE: les catégories des paramètres doivent avoir un nomERREUR INTERNE: les paramètres doivent avoir un nomIdentité trouvée: [%(category)s/%(type)s] %(identity)sImplémentation du protocole de transportsImplémentation de l'initialisation de flux pour le transfert de fichier Implémentation du « SOCKS5 Bytestreams » (flux d'octets SOCKS5)Implémentation de l'enregistrement en ligneImplementation de vcard-tempImpossible de contacter le site CouchSurfing, veuillez vérifier vos identifiant/mot de passe, votre connexion, ou essayez un peu plus tardERREUR INTERNE: état de jeu inconnuRejoindre un salonLancement du serveur de flux Socks5 sur le port %dIdentifiantFait un fichier compressé bzip2Initialisation du gestionnaire de mémoireMessageMessage envoyéMessagesDiversC'est à moi de jouerNouveauNouvel avatar trouvé, demande de vcardSurnonAucune donnée trouvéePas de profile par défaut, envoi du premierAucun transport trouvéPas de "keep_alife"Aucun profile sélectionnéL'utilisateur, le mot de passe ou le serveur n'ont pas été spécifiés, impossible d'inscrire un nouveau compte.En ligneGestion des paramètresPasseMot de passe:PetiteN° de Tél:Photo du type [%s] trouvéeLe joueur %(excuse_owner)s donne %(card_waited)s à %(player_waiting)s en compensation pour l'ExcuseLe joueur %(player)s est prêt à commencer [statut: %(status)s]Veuillez choisir votre contratVeuillez entrer le JID de votre nouveau contactVeuillez entrer le nom du nouveau profilePitiééééééééé, je ne peux même pas respirer !ExtensionInitialisation du plugin XEP_0054Initialisation du plugin XEP_0065Initialisation du plugin XEP_0077Initialisation du plugin XEP_0096Profile:Barre de progression désactivée -- Progression: ProgressBar n'est pas disponible, veuillez le télécharger à http://pypi.python.org/pypi/progressbarInitialisation du protocoleLe fichier [%(filename)s] de %(sender)s a été refusé: un fichier avec le même nom existe déjàEnregistrement d'un nouveau compteÉched de l'insciption (%s)Échec de l'inscription: %sInscription réussie, lancement du reste de la procédureInscription réussieDemande de suppression de contactRemplacement de l'utilisateur %sLe paramètre demandé [%(name)s] dans la catégorie [%(category)s] n'existe pas !Demande d'un paramètre pour un profile inconnuDemande d'un paramètre inconnu: (%(category)s/%(name)s)SauvegarderSauvegarde du nouveau mot de passeVeuillez sélectionner le profile à utiliserEnvoi un fichierSépare les messages xmpp: envoi un message par ligne plutôt qu'un seul message global.Demande d'affichage du profile d'un contactAffiche la barre de progressionAfficher le%(plural)s message%(plural)s non lu dans un navigateur webConnexion du client SOCKS5 perdue (raison: %s)Connexion du client SOCKS 5 démarréeConnexion du serveur SOCKS5 perdue (raison: %s)Connexion du serveur SOCKS 5 démarréeLancement de la compression, veuillez patienter...Demande de changement de statutSujetEnvoyerSuccèsL'attaquant (%(attaquant)s) fait %(points)i et joue pour %(point_limit)i (%(nb_bouts)s bout%(plural)s%(separator)s%(bouts)s): il %(victory)sLe contact %(jid)s veut vous envoyer le fichier %(filename)s Êtes vous d'accord ?Le contact %s a accepté votre inscriptionLe contact %s a refusé votre inscriptionLe contact %s veut s'inscrire à vos informations de présence Acceptez vous ?Les données du formulaire ne sont pas envoyées, il y a une erreur dans la gestion du typeLe message a été envoyé à tous les destinatairesle vainqueur de cette main est %sIl y a déjà un profile utilisé (nous comme en mode profile unique) !Il n'y a aucun destinataire pour ce message !Ce transport ne peut être gérée par SàT, désolé :(Cette extension vous permet de gérer votre compte CouchSurfing à travers votre frontend SàTCe profile n'est pas utiliséTransfert [%s] refuséClic sur l'icône de la barre de tâchesVous essayer d'utiliser une constante indéfinieTentative d'accès à un profile inconnuTentative d'ajout d'informations de présence à un profile inexistantVous essayer de connecter un profile qui n'existe pasTentative d'effacement d'une callback de progression inconnue.Messagerie inconnueProfil inconnu ou déconnecté (%s)élément de jeu de carte inconnu: %sDésinscription à la présence de %sInterrompu par l'utilisateur: au revoirCe nom d'utilisateur existe déjà, veuillez en choisir un autreVCard trouvéeAttend qu'un fichier soit envoyé par un contactSite WebOù voulez-vous sauvegarder le fichier ?client jabber WixLe flux XML est initialiséInterface utilisateur XML reçueconfirmation de type Oui/Non demandéeVous devez remplir vos identifiant & mot de passe CouchSurfing dans les paramètres avant d'utiliser cette interfaceVous n'avez sélectionné aucun contact !Vous perdez :(Vous devez sélectionner un profile ou en créer un nouveau avant de vous connecter.Vous devez être exactement 4 personnes dans le salon pour commencer un jeu de TarotVictoire \o/[%(profile)s] rejoint %(room)s avec %(nick)s[%s] est un répertoire ! Veuillez envoyer les fichiers qu'il contient ou utiliser la compression.[INTERNAL] tentative de supprimer un utilisateur pour une fenêtre de chat qui n'est pas un chat de groupe[INTERNAL] tentative de remplacer un utilisateur pour une fenêtre de chat qui n'est pas un chat de groupe[INTERNAL] tentative d'indiquer les utilisateurs présents pour une fenêtre de chat qui n'est pas un chat de groupe[INTERNAL] tentative de changer le sujet pour une fenêtre de chat qui n'est pas un chat de groupeacceptéactionResult: type = [%(type)s] id = [%(id)s] data = [%(data)s]demande de l'état de connexion pour un profile qui n'existe pasbz2 est une option expérimentale à un stade de développement peu avancé, utilisez-là avec prudencefermeturecontrat [%(contrat)s] choisi par %(profile)sSélection du contratfichier [%s] déjà en cachegetGame n'est pas implémenté dans ce frontendmessage reçu de: %sImportation du plugin: %sobject trouvé: %snom@serveur.extnouveau jeu de Tarot lancé par [%(referee)s] dans le salon [%(room_jid)s] avec %(players)snouveau contact: %sVous n'êtes pas connecté !Maintenant on affiche l'historiqueonContactActivated: %sLe paramètre [%(namespace)s] %(name)s vaut désormais %(value)sAnalyse de la vcardbranchement du profil %sMise à jour de l'information de présence pour [%(entity)s] (unavailable, statuses=%(statuses)s)le profil %s est inconnurefuséréponse à la demande d'inscription: %ssupppression du contact %sLancement de l'applicationpointsenvoyerEnvoi du message à %(friends)s avec le sujet [%(subject)s]Le paramètre %(name)s vaut désormais %(value)s dans la catégorie %(category)sConfiguration des parents des extensionsstartGame n'est pas implémenté dans ce frontendArrêt de l'applicationinscription approuvée pour [%s]id inconnue, on l'ignoredemande de désinscription pour [%s]mise à jour de %sÉcartsat-0.6.1.1+hg20180208/misc/0002755000175500017600000000000013243470024014727 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/misc/_jp0000644000175500017600000001240413243470024015421 0ustar debaclelocal_src#compdef jp jp_dev # jp: a SAT command line tool # Copyright (C) 2009-2016 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 . #TODO: - caching (see _store_cache en _retrieve_cache) # - filtering imposibles arguments # - arguments (jids, files) PYTHON='python2' local optionals subcommands arguments local context state state_descr line typeset -A val_args _jp() { eval `/usr/bin/env $PYTHON 2> /dev/null <<- PYTHONEND import re from subprocess import check_output # import sys # words_raw="jp_dev " + ' '.join(sys.argv[1:]) # for debugging in a script words_raw="$words" # $words is the command line currently completed words_all = words_raw.split() prog_name = words_all[0] words_no_opt = [word for word in words_all if not word.startswith('-')] # command line without optional arguments choices_cache = {} ARG = r'[-a-z0-9_]' # charset accepted for an argument name subcommands_re = re.compile(r"^ +{((?:" + ARG + r"+)(?:," + ARG + r"+)*)}", re.MULTILINE) optionals_re = re.compile(r"^ {2,}(--?" + ARG + r"+(?: [A-Z_0-9]+)?(?:, --" + ARG + r"+(?: [A-Z_0-9]+)?)?)\n? {2,}(.*(?:\n {4,}.*)*$)", re.MULTILINE) arguments_re = re.compile(r"^ {2,}([a-z_]" + ARG + r"*) {2,}(.*$)", re.MULTILINE) clean_re = re.compile(r"(?P^ +)|(?P {2,})|(?P\n)|(?P')|(?P +$)", re.MULTILINE) def _clean(desc): def sub_clean(match): matched_dict = match.groupdict() matched = {matched for matched in matched_dict if matched_dict[matched]} if matched.intersection(('prefix_spaces', 'suffix_spaces')): return '' elif matched.intersection(('double_spaces', 'newline')): return ' ' elif matched.intersection(('quote',)): return r"'\''" else: raise ValueError return clean_re.sub(sub_clean, desc) def parse_help(jp_help): # parse the help returning subcommands, optionals arguments, and mandatory arguments subcommands = subcommands_re.findall(jp_help) subcommands = {subcommand:"" for subcommand in subcommands[0].split(',')} if subcommands else {} optionals = dict(optionals_re.findall(jp_help)) arguments = dict(arguments_re.findall(jp_help)) for subcommand in subcommands: subcommands[subcommand] = arguments.pop(subcommand, '') return subcommands, optionals, arguments def get_choice(opt_choice): choices = choices_cache.get(opt_choice) if choices is not None: return choices if opt_choice == 'PROFILE': profiles = check_output([prog_name, 'profile', 'list']) choices = ":profile:(%s)" % ' '.join(profiles.split('\n')) if choices: choices_cache[opt_choice] = choices return choices else: return "" def construct_opt(opts, desc): # construct zsh's _arguments line for optional arguments arg_lines = [] for opt in opts.split(', '): try: opt_name, opt_choice = opt.split() except ValueError: # there is no argument opt_name, opt_choice = opt, None # arg_lines.append("'()%s[%s]%s'" % (opt_name+('=' if opt_name.startswith('--') else '+'), arg_lines.append("'()%s[%s]%s'" % (opt_name, _clean(desc), "%s" % get_choice(opt_choice) if opt_choice else '' )) return ' '.join(arg_lines) current_args = [] while True: # parse jp's help recursively until words_no_opt doesn't correspond anymore to a subcommand try: current_args.append(words_no_opt.pop(0)) jp_help = check_output(current_args + ['--help']) # print "jp_help (%s):\n%s\n\n---\n" % (' '.join(current_args), jp_help) # for debugging subcommands, optionals, arguments = parse_help(jp_help) if words_no_opt[0] not in subcommands: break except IndexError: break # now we fill the arrays so zsh can use them env=[] env.append("optionals=(%s)" % ' '.join(construct_opt(opt, desc) for opt, desc in optionals.items())) env.append("subcommands=(%s)" % ' '.join(["'%s[%s]'" % (subcommand, _clean(desc)) for subcommand, desc in subcommands.items()])) env.append("arguments=(%s)" % ' '.join(["'%s[%s]'" % (argument, _clean(desc)) for argument, desc in arguments.items()])) print ";".join(env) # this line is for eval PYTHONEND ` if [ -n "$optionals" ]; then _values optional $optionals fi if [ -n "$subcommands" ]; then _values subcommand $subcommands fi if [ -n "$arguments" ]; then #_values argument $arguments fi } _jp "$@" sat-0.6.1.1+hg20180208/misc/README0000644000175500017600000000124613243470024015610 0ustar debaclelocal_srcThis directory contains files related to SàT but not directly used by it. * file _jp: This is the completion file for zsh. To use it, you need to have it in a path accessible in your fpath variable, and to have completion activated. This can be done by the following commands in your .zshrc: ### .zshrc completion ### fpath=(/path/to/directory/with/_jp/ $fpath) autoload -U compinit compinit ### end of .zshrc completion ### Then, you should be able to complete a jp command line by pressing [TAB]. * org.goffi.SAT.service: This file is used by D-Bus to know how to launch the backend, you have to put in in D-Bus services dir (usually /usr/share/dbus-1/services) sat-0.6.1.1+hg20180208/misc/org.goffi.SAT.service0000644000175500017600000000005413243470024020614 0ustar debaclelocal_src[D-BUS Service] Name=org.goffi.SAT Exec=sat sat-0.6.1.1+hg20180208/ez_setup.py0000644000175500017600000002435413243470024016212 0ustar debaclelocal_src#!/usr/bin/env python """Bootstrap setuptools installation To use setuptools in your package's setup.py, include this file in the same directory and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() To require a specific version of setuptools, set a download mirror, or use an alternate download directory, simply supply the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import os import shutil import sys import tempfile import zipfile import optparse import subprocess import platform import textwrap import contextlib from distutils import log try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen try: from site import USER_SITE except ImportError: USER_SITE = None DEFAULT_VERSION = "5.5" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" def _python_cmd(*args): """ Return True if the command succeeded. """ args = (sys.executable,) + args return subprocess.call(args) == 0 def _install(archive_filename, install_args=()): with archive_context(archive_filename): # installing log.warn('Installing Setuptools') if not _python_cmd('setup.py', 'install', *install_args): log.warn('Something went wrong during the installation.') log.warn('See the error message above.') # exitcode will be 2 return 2 def _build_egg(egg, archive_filename, to_dir): with archive_context(archive_filename): # building an egg log.warn('Building a Setuptools egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') class ContextualZipFile(zipfile.ZipFile): """ Supplement ZipFile class to support context manager for Python 2.6 """ def __enter__(self): return self def __exit__(self, type, value, traceback): self.close() def __new__(cls, *args, **kwargs): """ Construct a ZipFile or ContextualZipFile as appropriate """ if hasattr(zipfile.ZipFile, '__exit__'): return zipfile.ZipFile(*args, **kwargs) return super(ContextualZipFile, cls).__new__(cls) @contextlib.contextmanager def archive_context(filename): # extracting the archive tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) with ContextualZipFile(filename) as archive: archive.extractall() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) yield finally: os.chdir(old_wd) shutil.rmtree(tmpdir) def _do_download(version, download_base, to_dir, download_delay): egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): archive = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, archive, to_dir) sys.path.insert(0, egg) # Remove previously-imported pkg_resources if present (see # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). if 'pkg_resources' in sys.modules: del sys.modules['pkg_resources'] import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15): to_dir = os.path.abspath(to_dir) rep_modules = 'pkg_resources', 'setuptools' imported = set(sys.modules).intersection(rep_modules) try: import pkg_resources except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: pkg_resources.require("setuptools>=" + version) return except pkg_resources.DistributionNotFound: return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.VersionConflict as VC_err: if imported: msg = textwrap.dedent(""" The required version of setuptools (>={version}) is not available, and can't be installed while this script is running. Please install a more recent version first, using 'easy_install -U setuptools'. (Currently using {VC_err.args[0]!r}) """).format(VC_err=VC_err, version=version) sys.stderr.write(msg) sys.exit(2) # otherwise, reload ok del pkg_resources, sys.modules['pkg_resources'] return _do_download(version, download_base, to_dir, download_delay) def _clean_check(cmd, target): """ Run the command to download target. If the command fails, clean up before re-raising the error. """ try: subprocess.check_call(cmd) except subprocess.CalledProcessError: if os.access(target, os.F_OK): os.unlink(target) raise def download_file_powershell(url, target): """ Download the file at url to target using Powershell (which will validate trust). Raise an exception if the command cannot complete. """ target = os.path.abspath(target) ps_cmd = ( "[System.Net.WebRequest]::DefaultWebProxy.Credentials = " "[System.Net.CredentialCache]::DefaultCredentials; " "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars() ) cmd = [ 'powershell', '-Command', ps_cmd, ] _clean_check(cmd, target) def has_powershell(): if platform.system() != 'Windows': return False cmd = ['powershell', '-Command', 'echo test'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_powershell.viable = has_powershell def download_file_curl(url, target): cmd = ['curl', url, '--silent', '--output', target] _clean_check(cmd, target) def has_curl(): cmd = ['curl', '--version'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_curl.viable = has_curl def download_file_wget(url, target): cmd = ['wget', url, '--quiet', '--output-document', target] _clean_check(cmd, target) def has_wget(): cmd = ['wget', '--version'] with open(os.path.devnull, 'wb') as devnull: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except Exception: return False return True download_file_wget.viable = has_wget def download_file_insecure(url, target): """ Use Python to download the file, even though it cannot authenticate the connection. """ src = urlopen(url) try: # Read all the data in one block. data = src.read() finally: src.close() # Write all the data in one block to avoid creating a partial file. with open(target, "wb") as dst: dst.write(data) download_file_insecure.viable = lambda: True def get_best_downloader(): downloaders = ( download_file_powershell, download_file_curl, download_file_wget, download_file_insecure, ) viable_downloaders = (dl for dl in downloaders if dl.viable()) return next(viable_downloaders, None) def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): """ Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an sdist for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. ``downloader_factory`` should be a function taking no arguments and returning a function for downloading a URL to a target. """ # making sure we use the absolute path to_dir = os.path.abspath(to_dir) zip_name = "setuptools-%s.zip" % version url = download_base + zip_name saveto = os.path.join(to_dir, zip_name) if not os.path.exists(saveto): # Avoid repeated downloads log.warn("Downloading %s", url) downloader = downloader_factory() downloader(url, saveto) return os.path.realpath(saveto) def _build_install_args(options): """ Build the arguments to 'python setup.py install' on the setuptools package """ return ['--user'] if options.user_install else [] def _parse_args(): """ Parse the command line for options """ parser = optparse.OptionParser() parser.add_option( '--user', dest='user_install', action='store_true', default=False, help='install in user site package (requires Python 2.6 or later)') parser.add_option( '--download-base', dest='download_base', metavar="URL", default=DEFAULT_URL, help='alternative URL from where to download the setuptools package') parser.add_option( '--insecure', dest='downloader_factory', action='store_const', const=lambda: download_file_insecure, default=get_best_downloader, help='Use internal, non-validating downloader' ) parser.add_option( '--version', help="Specify which version to download", default=DEFAULT_VERSION, ) options, args = parser.parse_args() # positional arguments are ignored return options def main(): """Install or upgrade setuptools and EasyInstall""" options = _parse_args() archive = download_setuptools( version=options.version, download_base=options.download_base, downloader_factory=options.downloader_factory, ) return _install(archive, _build_install_args(options)) if __name__ == '__main__': sys.exit(main()) sat-0.6.1.1+hg20180208/src/0002755000175500017600000000000013243470025014564 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/test/0002755000175500017600000000000013243470025015543 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0277.py0000644000175500017600000001174513243470025022013 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin XEP-0277 tests """ from sat.test import helpers from sat.plugins import plugin_xep_0277 from sat.plugins import plugin_xep_0060 from sat.plugins import plugin_misc_text_syntaxes from sat.tools.xml_tools import ElementParser from wokkel.pubsub import NS_PUBSUB class XEP_0277Test(helpers.SatTestCase): PUBSUB_ENTRY_1 = u""" <span>titre</span> c745a688-9b02-11e3-a1a3-c0143dd4fe51 2014-02-21T16:16:39+02:00 2014-02-21T16:16:38+02:00 <p>contenu</p>texte sans balise<p>autre contenu</p>

contenu

texte sans balise

autre contenu

test1@souliane.org
""" % plugin_xep_0277.NS_ATOM PUBSUB_ENTRY_2 = u""" <div>titre</div> <div xmlns="http://www.w3.org/1999/xhtml"><div style="background-image: url('xxx');">titre</div></div> c745a688-9b02-11e3-a1a3-c0143dd4fe51 2014-02-21T16:16:39+02:00 2014-02-21T16:16:38+02:00 <div><p>contenu</p>texte dans balise<p>autre contenu</p></div>

contenu

texte dans balise

autre contenu

test1@souliane.org test1
""" % plugin_xep_0277.NS_ATOM def setUp(self): self.host = helpers.FakeSAT() class XEP_0163(object): def __init__(self, host): pass def addPEPEvent(self, *args): pass self.host.plugins["XEP-0060"] = plugin_xep_0060.XEP_0060(self.host) self.host.plugins["XEP-0163"] = XEP_0163(self.host) reload(plugin_misc_text_syntaxes) # reload the plugin to avoid conflict error self.host.plugins["TEXT-SYNTAXES"] = plugin_misc_text_syntaxes.TextSyntaxes(self.host) self.plugin = plugin_xep_0277.XEP_0277(self.host) def test_item2mbdata_1(self): expected = {u'id': u'c745a688-9b02-11e3-a1a3-c0143dd4fe51', u'atom_id': u'c745a688-9b02-11e3-a1a3-c0143dd4fe51', u'title': u'titre', u'updated': u'1392992199.0', u'published': u'1392992198.0', u'content': u'

contenu

texte sans balise

autre contenu

', u'content_xhtml': u'

contenu

texte sans balise

autre contenu

', u'author': u'test1@souliane.org' } item_elt = ElementParser()(self.PUBSUB_ENTRY_1, namespace=NS_PUBSUB).elements().next() d = self.plugin.item2mbdata(item_elt) d.addCallback(self.assertEqual, expected) return d def test_item2mbdata_2(self): expected = {u'id': u'c745a688-9b02-11e3-a1a3-c0143dd4fe51', u'atom_id': u'c745a688-9b02-11e3-a1a3-c0143dd4fe51', u'title': u'
titre
', u'title_xhtml': u'
titre
', u'updated': u'1392992199.0', u'published': u'1392992198.0', u'content': u'

contenu

texte dans balise

autre contenu

', u'content_xhtml': u'

contenu

texte dans balise

autre contenu

', u'author': u'test1@souliane.org' } item_elt = ElementParser()(self.PUBSUB_ENTRY_2, namespace=NS_PUBSUB).elements().next() d = self.plugin.item2mbdata(item_elt) d.addCallback(self.assertEqual, expected) return d sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0334.py0000644000175500017600000000701213243470025021775 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin XEP-0334 """ from constants import Const as C from sat.test import helpers from sat.plugins.plugin_xep_0334 import XEP_0334 from twisted.internet import defer from wokkel.generic import parseXml from sat.core import exceptions HINTS = ('no-permanent-storage', 'no-storage', 'no-copy') class XEP_0334Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = XEP_0334(self.host) def test_messageSendTrigger(self): template_xml = """ text %s """ original_xml = template_xml % '' d_list = [] def cb(data, expected_xml): result_xml = data['xml'].toXml().encode("utf-8") self.assertEqualXML(result_xml, expected_xml, True) for key in (HINTS + ('', 'dummy_hint')): mess_data = {'xml': parseXml(original_xml.encode("utf-8")), 'extra': {key: True} } treatments = defer.Deferred() self.plugin.messageSendTrigger(self.host.getClient(C.PROFILE[0]), mess_data, defer.Deferred(), treatments) if treatments.callbacks: # the trigger added a callback expected_xml = template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key) treatments.addCallback(cb, expected_xml) treatments.callback(mess_data) d_list.append(treatments) return defer.DeferredList(d_list) def test_messageReceivedTrigger(self): template_xml = """ text %s """ def cb(dummy): raise Exception("Errback should not be ran instead of callback!") def eb(failure): failure.trap(exceptions.SkipHistory) d_list = [] for key in (HINTS + ('dummy_hint',)): message = parseXml(template_xml % ('<%s xmlns="urn:xmpp:hints"/>' % key)) post_treat = defer.Deferred() self.plugin.messageReceivedTrigger(self.host.getClient(C.PROFILE[0]), message, post_treat) if post_treat.callbacks: assert(key in ('no-permanent-storage', 'no-storage')) post_treat.addCallbacks(cb, eb) post_treat.callback(None) d_list.append(post_treat) else: assert(key not in ('no-permanent-storage', 'no-storage')) return defer.DeferredList(d_list) sat-0.6.1.1+hg20180208/src/test/test_plugin_misc_radiocol.py0000644000175500017600000004535513243470025023353 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009, 2010, 2011, 2012, 2013 Jérôme Poisson (goffi@goffi.org) # Copyright (C) 2013 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 . """ Tests for the plugin radiocol """ from sat.core import exceptions from sat.test import helpers, helpers_plugins from sat.plugins import plugin_misc_radiocol as plugin from sat.plugins import plugin_misc_room_game as plugin_room_game from constants import Const from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish from twisted.internet import reactor from twisted.internet import defer from twisted.python.failure import Failure from twisted.trial.unittest import SkipTest try: from mutagen.oggvorbis import OggVorbis from mutagen.mp3 import MP3 from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3NoHeaderError except ImportError: raise exceptions.MissingModule(u"Missing module Mutagen, please download/install from https://bitbucket.org/lazka/mutagen") import uuid import os import copy import shutil ROOM_JID = JID(Const.MUC_STR[0]) PROFILE = Const.PROFILE[0] REFEREE_FULL = JID(ROOM_JID.userhost() + '/' + Const.JID[0].user) PLAYERS_INDICES = [0, 1, 3] # referee included OTHER_PROFILES = [Const.PROFILE[1], Const.PROFILE[3]] OTHER_PLAYERS = [Const.JID[1], Const.JID[3]] class RadiocolTest(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() def reinit(self): self.host.reinit() self.host.plugins['ROOM-GAME'] = plugin_room_game.RoomGame(self.host) self.plugin = plugin.Radiocol(self.host) # must be init after ROOM-GAME self.plugin.testing = True self.plugin_0045 = self.host.plugins['XEP-0045'] = helpers_plugins.FakeXEP_0045(self.host) self.plugin_0249 = self.host.plugins['XEP-0249'] = helpers_plugins.FakeXEP_0249(self.host) for profile in Const.PROFILE: self.host.getClient(profile) # init self.host.profiles[profile] self.songs = [] self.playlist = [] self.sound_dir = self.host.memory.getConfig('', 'media_dir') + '/test/sound/' try: for filename in os.listdir(self.sound_dir): if filename.endswith('.ogg') or filename.endswith('.mp3'): self.songs.append(filename) except OSError: raise SkipTest('The sound samples in sat_media/test/sound were not found') def _buildPlayers(self, players=[]): """@return: the "started" content built with the given players""" content = "<%s xmlns='%s'>%s" % (to_jid.full(), type_, plugin.RADIOC_TAG, plugin.NC_RADIOCOL, content, plugin.RADIOC_TAG) def _rejectSongCb(self, profile_index): """Check if the message "song_rejected" has been sent by the referee and process the command with the profile of the uploader @param profile_index: uploader's profile""" sent = self.host.getSentMessage(0) content = "" self.assertEqualXML(sent.toXml(), self._expectedMessage(JID(ROOM_JID.userhost() + '/' + self.plugin_0045.getNick(0, profile_index), 'normal', content))) self._roomGameCmd(sent, ['radiocolSongRejected', ROOM_JID.full(), 'Too many songs in queue']) def _noUploadCb(self): """Check if the message "no_upload" has been sent by the referee and process the command with the profiles of each room users""" sent = self.host.getSentMessage(0) content = "" self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content)) self._roomGameCmd(sent, ['radiocolNoUpload', ROOM_JID.full()]) def _uploadOkCb(self): """Check if the message "upload_ok" has been sent by the referee and process the command with the profiles of each room users""" sent = self.host.getSentMessage(0) content = "" self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content)) self._roomGameCmd(sent, ['radiocolUploadOk', ROOM_JID.full()]) def _preloadCb(self, attrs, profile_index): """Check if the message "preload" has been sent by the referee and process the command with the profiles of each room users @param attrs: information dict about the song @param profile_index: profile index of the uploader """ sent = self.host.getSentMessage(0) attrs['sender'] = self.plugin_0045.getNick(0, profile_index) radiocol_elt = domish.generateElementsNamed(sent.elements(), 'radiocol').next() preload_elt = domish.generateElementsNamed(radiocol_elt.elements(), 'preload').next() attrs['timestamp'] = preload_elt['timestamp'] # we could not guess it... content = "" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs]) if sent.hasAttribute('from'): del sent['from'] self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content)) self._roomGameCmd(sent, ['radiocolPreload', ROOM_JID.full(), attrs['timestamp'], attrs['filename'], attrs['title'], attrs['artist'], attrs['album'], attrs['sender']]) def _playNextSongCb(self): """Check if the message "play" has been sent by the referee and process the command with the profiles of each room users""" sent = self.host.getSentMessage(0) filename = self.playlist.pop(0) content = "" % filename self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', content)) self._roomGameCmd(sent, ['radiocolPlay', ROOM_JID.full(), filename]) game_data = self.plugin.games[ROOM_JID] if len(game_data['queue']) == plugin.QUEUE_LIMIT - 1: self._uploadOkCb() def _addSongCb(self, d, filepath, profile_index): """Check if the message "song_added" has been sent by the uploader and process the command with the profile of the referee @param d: deferred value or failure got from self.plugin.radiocolSongAdded @param filepath: full path to the sound file @param profile_index: the profile index of the uploader """ if isinstance(d, Failure): self.fail("OGG or MP3 song could not be added!") game_data = self.plugin.games[ROOM_JID] # this is copied from the plugin if filepath.lower().endswith('.mp3'): actual_song = MP3(filepath) try: song = EasyID3(filepath) class Info(object): def __init__(self, length): self.length = length song.info = Info(actual_song.info.length) except ID3NoHeaderError: song = actual_song else: song = OggVorbis(filepath) attrs = {'filename': os.path.basename(filepath), 'title': song.get("title", ["Unknown"])[0], 'artist': song.get("artist", ["Unknown"])[0], 'album': song.get("album", ["Unknown"])[0], 'length': str(song.info.length) } self.assertEqual(game_data['to_delete'][attrs['filename']], filepath) content = "" % " ".join(["%s='%s'" % (attr, attrs[attr]) for attr in attrs]) sent = self.host.getSentMessage(profile_index) self.assertEqualXML(sent.toXml(), self._expectedMessage(REFEREE_FULL, 'normal', content)) reject_song = len(game_data['queue']) >= plugin.QUEUE_LIMIT no_upload = len(game_data['queue']) + 1 >= plugin.QUEUE_LIMIT play_next = not game_data['playing'] and len(game_data['queue']) + 1 == plugin.QUEUE_TO_START self._roomGameCmd(sent, profile_index) # queue unchanged or +1 if reject_song: self._rejectSongCb(profile_index) return if no_upload: self._noUploadCb() self._preloadCb(attrs, profile_index) self.playlist.append(attrs['filename']) if play_next: self._playNextSongCb() # queue -1 def _roomGameCmd(self, sent, from_index=0, call=[]): """Process a command. It is also possible to call this method as _roomGameCmd(sent, call) instead of _roomGameCmd(sent, from_index, call). If from index is a list, it is assumed that it is containing the value for call and from_index will take its default value. @param sent: the sent message that we need to process @param from_index: index of the message sender @param call: list containing the name of the expected bridge call followed by its arguments, or empty list if no call is expected """ if isinstance(from_index, list): call = from_index from_index = 0 sent['from'] = ROOM_JID.full() + '/' + self.plugin_0045.getNick(0, from_index) recipient = JID(sent['to']).resource # The message could have been sent to a room user (room_jid + '/' + nick), # but when it is received, the 'to' attribute of the message has been # changed to the recipient own JID. We need to simulate that here. if recipient: room = self.plugin_0045.getRoom(0, 0) sent['to'] = Const.JID_STR[0] if recipient == room.nick else room.roster[recipient].entity.full() for index in xrange(0, len(Const.PROFILE)): nick = self.plugin_0045.getNick(0, index) if nick: if not recipient or nick == recipient: if call and (self.plugin.isPlayer(ROOM_JID, nick) or call[0] == 'radiocolStarted'): args = copy.deepcopy(call) args.append(Const.PROFILE[index]) self.host.bridge.expectCall(*args) self.plugin.room_game_cmd(sent, Const.PROFILE[index]) def _syncCb(self, sync_data, profile_index): """Synchronize one player when he joins a running game. @param sync_data: result from self.plugin.getSyncData @param profile_index: index of the profile to be synchronized """ for nick in sync_data: expected = self._expectedMessage(JID(ROOM_JID.userhost() + '/' + nick), 'normal', sync_data[nick]) sent = self.host.getSentMessage(0) self.assertEqualXML(sent.toXml(), expected) for elt in sync_data[nick]: if elt.name == 'preload': self.host.bridge.expectCall('radiocolPreload', ROOM_JID.full(), elt['timestamp'], elt['filename'], elt['title'], elt['artist'], elt['album'], elt['sender'], Const.PROFILE[profile_index]) elif elt.name == 'play': self.host.bridge.expectCall('radiocolPlay', ROOM_JID.full(), elt['filename'], Const.PROFILE[profile_index]) elif elt.name == 'no_upload': self.host.bridge.expectCall('radiocolNoUpload', ROOM_JID.full(), Const.PROFILE[profile_index]) sync_data[nick] self._roomGameCmd(sent, []) def _joinRoom(self, room, nicks, player_index, sync=True): """Make a player join a room and update the list of nicks @param room: wokkel.muc.Room instance from the referee perspective @param nicks: list of the players which will be updated @param player_index: profile index of the new player @param sync: set to True to synchronize data """ user_nick = self.plugin_0045.joinRoom(0, player_index) self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE) if player_index not in PLAYERS_INDICES: # this user is actually not a player self.assertFalse(self.plugin.isPlayer(ROOM_JID, user_nick)) to_jid, type_ = (JID(ROOM_JID.userhost() + '/' + user_nick), 'normal') else: # this user is a player self.assertTrue(self.plugin.isPlayer(ROOM_JID, user_nick)) nicks.append(user_nick) to_jid, type_ = (ROOM_JID, 'groupchat') # Check that the message "players" has been sent by the referee expected = self._expectedMessage(to_jid, type_, self._buildPlayers(nicks)) sent = self.host.getSentMessage(0) self.assertEqualXML(sent.toXml(), expected) # Process the command with the profiles of each room users self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID.full(), REFEREE_FULL.full(), nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]]) if sync: self._syncCb(self.plugin._getSyncData(ROOM_JID, [user_nick]), player_index) def _leaveRoom(self, room, nicks, player_index): """Make a player leave a room and update the list of nicks @param room: wokkel.muc.Room instance from the referee perspective @param nicks: list of the players which will be updated @param player_index: profile index of the new player """ user_nick = self.plugin_0045.getNick(0, player_index) user = room.roster[user_nick] self.plugin_0045.leaveRoom(0, player_index) self.plugin.userLeftTrigger(room, user, PROFILE) nicks.remove(user_nick) def _uploadSong(self, song_index, profile_index): """Upload the song of index song_index (modulo self.songs size) from the profile of index profile_index. @param song_index: index of the song or None to test with non existing file @param profile_index: index of the uploader's profile """ if song_index is None: dst_filepath = unicode(uuid.uuid1()) expect_io_error = True else: song_index = song_index % len(self.songs) src_filename = self.songs[song_index] dst_filepath = '/tmp/%s%s' % (uuid.uuid1(), os.path.splitext(src_filename)[1]) shutil.copy(self.sound_dir + src_filename, dst_filepath) expect_io_error = False try: d = self.plugin.radiocolSongAdded(REFEREE_FULL, dst_filepath, Const.PROFILE[profile_index]) except IOError: self.assertTrue(expect_io_error) return self.assertFalse(expect_io_error) cb = lambda defer: self._addSongCb(defer, dst_filepath, profile_index) def eb(failure): if not isinstance(failure, Failure): self.fail("Adding a song which is not OGG nor MP3 should fail!") self.assertEqual(failure.value.__class__, exceptions.DataError) if src_filename.endswith('.ogg') or src_filename.endswith('.mp3'): d.addCallbacks(cb, cb) else: d.addCallbacks(eb, eb) def test_init(self): self.reinit() self.assertEqual(self.plugin.invite_mode, self.plugin.FROM_PLAYERS) self.assertEqual(self.plugin.wait_mode, self.plugin.FOR_NONE) self.assertEqual(self.plugin.join_mode, self.plugin.INVITED) self.assertEqual(self.plugin.ready_mode, self.plugin.FORCE) def test_game(self): self.reinit() # create game self.plugin.prepareRoom(OTHER_PLAYERS, ROOM_JID, PROFILE) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) room = self.plugin_0045.getRoom(0, 0) nicks = [self.plugin_0045.getNick(0, 0)] sent = self.host.getSentMessage(0) self.assertEqualXML(sent.toXml(), self._expectedMessage(ROOM_JID, 'groupchat', self._buildPlayers(nicks))) self._roomGameCmd(sent, ['radiocolStarted', ROOM_JID.full(), REFEREE_FULL.full(), nicks, [plugin.QUEUE_TO_START, plugin.QUEUE_LIMIT]]) self._joinRoom(room, nicks, 1) # player joins self._joinRoom(room, nicks, 4) # user not playing joins song_index = 0 self._uploadSong(song_index, 0) # ogg or mp3 file should exist in sat_media/test/song self._uploadSong(None, 0) # non existing file # another songs are added by Const.JID[1] until the radio starts + 1 to fill the queue # when the first song starts + 1 to be rejected because the queue is full for song_index in xrange(1, plugin.QUEUE_TO_START + 1): self._uploadSong(song_index, 1) self.plugin.playNext(Const.MUC[0], PROFILE) # simulate the end of the first song self._playNextSongCb() self._uploadSong(song_index, 1) # now the song is accepted and the queue is full again self._joinRoom(room, nicks, 3) # new player joins self.plugin.playNext(Const.MUC[0], PROFILE) # the second song finishes self._playNextSongCb() self._uploadSong(0, 3) # the player who recently joined re-upload the first file self._leaveRoom(room, nicks, 1) # one player leaves self._joinRoom(room, nicks, 1) # and join again self.plugin.playNext(Const.MUC[0], PROFILE) # empty the queue self._playNextSongCb() self.plugin.playNext(Const.MUC[0], PROFILE) self._playNextSongCb() for filename in self.playlist: self.plugin.deleteFile('/tmp/' + filename) return defer.succeed(None) def tearDown(self, *args, **kwargs): """Clean the reactor""" helpers.SatTestCase.tearDown(self, *args, **kwargs) for delayed_call in reactor.getDelayedCalls(): delayed_call.cancel() sat-0.6.1.1+hg20180208/src/test/test_memory_crypto.py0000644000175500017600000000502213243470025022061 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2016 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 . """ Tests for the plugin radiocol """ from sat.test import helpers from sat.memory.crypto import BlockCipher, PasswordHasher import random import string from twisted.internet import defer def getRandomUnicode(len): """Return a random unicode string""" return u''.join(random.choice(string.letters + u"éáúóâêûôßüöä") for i in xrange(len)) class CryptoTest(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() def test_encrypt_decrypt(self): d_list = [] def test(key, message): d = BlockCipher.encrypt(key, message) d.addCallback(lambda ciphertext: BlockCipher.decrypt(key, ciphertext)) d.addCallback(lambda decrypted: self.assertEqual(message, decrypted)) d_list.append(d) for key_len in (0, 2, 8, 10, 16, 24, 30, 32, 40): key = getRandomUnicode(key_len) for message_len in (0, 2, 16, 24, 32, 100): message = getRandomUnicode(message_len) test(key, message) return defer.DeferredList(d_list) def test_hash_verify(self): d_list = [] for password in (0, 2, 8, 10, 16, 24, 30, 32, 40): d = PasswordHasher.hash(password) def cb(hashed): d1 = PasswordHasher.verify(password, hashed) d1.addCallback(lambda result: self.assertTrue(result)) d_list.append(d1) attempt = getRandomUnicode(10) d2 = PasswordHasher.verify(attempt, hashed) d2.addCallback(lambda result: self.assertFalse(result)) d_list.append(d2) d.addCallback(cb) return defer.DeferredList(d_list) sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0203.py0000644000175500017600000000431213243470025021770 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin XEP-0203 """ from sat.test import helpers from sat.plugins.plugin_xep_0203 import XEP_0203 from twisted.words.xish import domish from twisted.words.protocols.jabber.jid import JID from dateutil.tz import tzutc import datetime NS_PUBSUB = 'http://jabber.org/protocol/pubsub' class XEP_0203Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = XEP_0203(self.host) def test_delay(self): delay_xml = """ Offline Storage """ message_xml = """ text %s """ % delay_xml parent = domish.Element((None, 'message')) parent['from'] = 'romeo@montague.net/orchard' parent['to'] = 'juliet@capulet.com' parent['type'] = 'chat' parent.addElement('body', None, 'text') stamp = datetime.datetime(2002, 9, 10, 23, 8, 25, tzinfo=tzutc()) elt = self.plugin.delay(stamp, JID('capulet.com'), 'Offline Storage', parent) self.assertEqualXML(elt.toXml(), delay_xml, True) self.assertEqualXML(parent.toXml(), message_xml, True) sat-0.6.1.1+hg20180208/src/test/constants.py0000644000175500017600000000324113243470025020127 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # Primitivus: a SAT frontend # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from twisted.words.protocols.jabber import jid class Const(object): PROF_KEY_NONE = '@NONE@' PROFILE = ['test_profile', 'test_profile2', 'test_profile3', 'test_profile4', 'test_profile5'] JID_STR = [u"test@example.org/SàT", u"sender@example.net/house", u"sender@example.net/work", u"sender@server.net/res", u"xxx@server.net/res"] JID = [jid.JID(jid_s) for jid_s in JID_STR] PROFILE_DICT = {} for i in xrange(0, len(PROFILE)): PROFILE_DICT[PROFILE[i]] = JID[i] MUC_STR = [u"room@chat.server.domain", u"sat_game@chat.server.domain"] MUC = [jid.JID(jid_s) for jid_s in MUC_STR] NO_SECURITY_LIMIT = -1 SECURITY_LIMIT = 0 # To test frontend parameters APP_NAME = "dummy_frontend" COMPOSITION_KEY = D_("Composition") ENABLE_UNIBOX_PARAM = D_("Enable unibox") PARAM_IN_QUOTES = D_("'Wysiwyg' edition") sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0297.py0000644000175500017600000000571613243470025022016 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin XEP-0297 """ from constants import Const as C from sat.test import helpers from sat.plugins.plugin_xep_0203 import XEP_0203 from sat.plugins.plugin_xep_0297 import XEP_0297 from twisted.words.protocols.jabber.jid import JID from dateutil.tz import tzutc import datetime from wokkel.generic import parseXml NS_PUBSUB = 'http://jabber.org/protocol/pubsub' class XEP_0297Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = XEP_0297(self.host) self.host.plugins['XEP-0203'] = XEP_0203(self.host) def test_delay(self): stanza = parseXml(""" Yet I should kill thee with much cherishing. """.encode('utf-8')) output = """ A most courteous exposition! Yet I should kill thee with much cherishing. """ stamp = datetime.datetime(2010, 7, 10, 23, 8, 25, tzinfo=tzutc()) d = self.plugin.forward(stanza, JID('mercutio@verona.lit'), stamp, body='A most courteous exposition!', profile_key=C.PROFILE[0]) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), output, True)) return d sat-0.6.1.1+hg20180208/src/test/test_memory.py0000644000175500017600000003074013243470025020466 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.test import helpers from twisted.trial import unittest import traceback from constants import Const from xml.dom import minidom class MemoryTest(unittest.TestCase): def setUp(self): self.host = helpers.FakeSAT() def _getParamXML(self, param="1", security_level=None): """Generate XML for testing parameters @param param (str): a subset of "123" @param security_level: security level of the parameters @return (str) """ def getParam(name): return """ """ % {'param_name': name, 'param_label': _(name), 'security': '' if security_level is None else ('security="%d"' % security_level) } params = '' if "1" in param: params += getParam(Const.ENABLE_UNIBOX_PARAM) if "2" in param: params += getParam(Const.PARAM_IN_QUOTES) if "3" in param: params += getParam("Dummy param") return """ %(params)s """ % { 'category_name': Const.COMPOSITION_KEY, 'category_label': _(Const.COMPOSITION_KEY), 'params': params } def _paramExists(self, param="1", src=None): """ @param param (str): a character in "12" @param src (DOM element): the top-level element to look in @return: True is the param exists """ if param == "1": name = Const.ENABLE_UNIBOX_PARAM else: name = Const.PARAM_IN_QUOTES category = Const.COMPOSITION_KEY if src is None: src = self.host.memory.params.dom.documentElement for type_node in src.childNodes: # when src comes self.host.memory.params.dom, we have here # some "individual" or "general" elements, when it comes # from Memory.getParams we have here a "params" elements if type_node.nodeName not in ("individual", "general", "params"): continue for cat_node in type_node.childNodes: if cat_node.nodeName != "category" or cat_node.getAttribute("name") != category: continue for param in cat_node.childNodes: if param.nodeName == "param" and param.getAttribute("name") == name: return True return False def assertParam_generic(self, param="1", src=None, exists=True, deferred=False): """ @param param (str): a character in "12" @param src (DOM element): the top-level element to look in @param exists (boolean): True to assert the param exists, False to assert it doesn't @param deferred (boolean): True if this method is called from a Deferred callback """ msg = "Expected parameter not found!\n" if exists else "Unexpected parameter found!\n" if deferred: # in this stack we can see the line where the error came from, # if limit=5, 6 is not enough you can increase the value msg += "\n".join(traceback.format_stack(limit=5 if exists else 6)) assertion = self._paramExists(param, src) getattr(self, "assert%s" % exists)(assertion, msg) def assertParamExists(self, param="1", src=None): self.assertParam_generic(param, src, True) def assertParamNotExists(self, param="1", src=None): self.assertParam_generic(param, src, False) def assertParamExists_async(self, src, param="1"): """@param src: a deferred result from Memory.getParams""" self.assertParam_generic(param, minidom.parseString(src.encode("utf-8")), True, True) def assertParamNotExists_async(self, src, param="1"): """@param src: a deferred result from Memory.getParams""" self.assertParam_generic(param, minidom.parseString(src.encode("utf-8")), False, True) def _getParams(self, security_limit, app='', profile_key='@NONE@'): """Get the parameters accessible with the given security limit and application name. @param security_limit (int): the security limit @param app (str): empty string or "libervia" @param profile_key """ if profile_key == '@NONE@': profile_key = '@DEFAULT@' return self.host.memory.params.getParams(security_limit, app, profile_key) def test_updateParams(self): self.host.memory.reinit() # check if the update works self.host.memory.updateParams(self._getParamXML()) self.assertParamExists() previous = self.host.memory.params.dom.cloneNode(True) # now check if it is really updated and not duplicated self.host.memory.updateParams(self._getParamXML()) self.assertEqual(previous.toxml().encode("utf-8"), self.host.memory.params.dom.toxml().encode("utf-8")) self.host.memory.reinit() # check successive updates (without intersection) self.host.memory.updateParams(self._getParamXML('1')) self.assertParamExists("1") self.assertParamNotExists("2") self.host.memory.updateParams(self._getParamXML('2')) self.assertParamExists("1") self.assertParamExists("2") previous = self.host.memory.params.dom.cloneNode(True) # save for later self.host.memory.reinit() # check successive updates (with intersection) self.host.memory.updateParams(self._getParamXML('1')) self.assertParamExists("1") self.assertParamNotExists("2") self.host.memory.updateParams(self._getParamXML('12')) self.assertParamExists("1") self.assertParamExists("2") # successive updates with or without intersection should have the same result self.assertEqual(previous.toxml().encode("utf-8"), self.host.memory.params.dom.toxml().encode("utf-8")) self.host.memory.reinit() # one update with two params in a new category self.host.memory.updateParams(self._getParamXML('12')) self.assertParamExists("1") self.assertParamExists("2") def test_getParams(self): # tests with no security level on the parameter (most secure) params = self._getParamXML() self.host.memory.reinit() self.host.memory.updateParams(params) self._getParams(Const.NO_SECURITY_LIMIT).addCallback(self.assertParamExists_async) self._getParams(0).addCallback(self.assertParamNotExists_async) self._getParams(1).addCallback(self.assertParamNotExists_async) # tests with security level 0 on the parameter (not secure) params = self._getParamXML(security_level=0) self.host.memory.reinit() self.host.memory.updateParams(params) self._getParams(Const.NO_SECURITY_LIMIT).addCallback(self.assertParamExists_async) self._getParams(0).addCallback(self.assertParamExists_async) self._getParams(1).addCallback(self.assertParamExists_async) # tests with security level 1 on the parameter (more secure) params = self._getParamXML(security_level=1) self.host.memory.reinit() self.host.memory.updateParams(params) self._getParams(Const.NO_SECURITY_LIMIT).addCallback(self.assertParamExists_async) self._getParams(0).addCallback(self.assertParamNotExists_async) return self._getParams(1).addCallback(self.assertParamExists_async) def test_paramsRegisterApp(self): def register(xml, security_limit, app): """ @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 """ helpers.muteLogging() self.host.memory.paramsRegisterApp(xml, security_limit, app) helpers.unmuteLogging() # tests with no security level on the parameter (most secure) params = self._getParamXML() self.host.memory.reinit() register(params, Const.NO_SECURITY_LIMIT, Const.APP_NAME) self.assertParamExists() self.host.memory.reinit() register(params, 0, Const.APP_NAME) self.assertParamNotExists() self.host.memory.reinit() register(params, 1, Const.APP_NAME) self.assertParamNotExists() # tests with security level 0 on the parameter (not secure) params = self._getParamXML(security_level=0) self.host.memory.reinit() register(params, Const.NO_SECURITY_LIMIT, Const.APP_NAME) self.assertParamExists() self.host.memory.reinit() register(params, 0, Const.APP_NAME) self.assertParamExists() self.host.memory.reinit() register(params, 1, Const.APP_NAME) self.assertParamExists() # tests with security level 1 on the parameter (more secure) params = self._getParamXML(security_level=1) self.host.memory.reinit() register(params, Const.NO_SECURITY_LIMIT, Const.APP_NAME) self.assertParamExists() self.host.memory.reinit() register(params, 0, Const.APP_NAME) self.assertParamNotExists() self.host.memory.reinit() register(params, 1, Const.APP_NAME) self.assertParamExists() # tests with security level 1 and several parameters being registered params = self._getParamXML("12", security_level=1) self.host.memory.reinit() register(params, Const.NO_SECURITY_LIMIT, Const.APP_NAME) self.assertParamExists() self.assertParamExists("2") self.host.memory.reinit() register(params, 0, Const.APP_NAME) self.assertParamNotExists() self.assertParamNotExists("2") self.host.memory.reinit() register(params, 1, Const.APP_NAME) self.assertParamExists() self.assertParamExists("2") # tests with several parameters being registered in an existing category self.host.memory.reinit() self.host.memory.updateParams(self._getParamXML("3")) register(self._getParamXML("12"), Const.NO_SECURITY_LIMIT, Const.APP_NAME) self.assertParamExists() self.assertParamExists("2") self.host.memory.reinit() def test_paramsRegisterApp_getParams(self): # test retrieving the parameter for a specific frontend self.host.memory.reinit() params = self._getParamXML(security_level=1) self.host.memory.paramsRegisterApp(params, 1, Const.APP_NAME) self._getParams(1, '').addCallback(self.assertParamExists_async) self._getParams(1, Const.APP_NAME).addCallback(self.assertParamExists_async) self._getParams(1, 'another_dummy_frontend').addCallback(self.assertParamNotExists_async) # the same with several parameters registered at the same time self.host.memory.reinit() params = self._getParamXML('12', security_level=0) self.host.memory.paramsRegisterApp(params, 5, Const.APP_NAME) self._getParams(5, '').addCallback(self.assertParamExists_async) self._getParams(5, '').addCallback(self.assertParamExists_async, "2") self._getParams(5, Const.APP_NAME).addCallback(self.assertParamExists_async) self._getParams(5, Const.APP_NAME).addCallback(self.assertParamExists_async, "2") self._getParams(5, 'another_dummy_frontend').addCallback(self.assertParamNotExists_async) return self._getParams(5, 'another_dummy_frontend').addCallback(self.assertParamNotExists_async, "2") sat-0.6.1.1+hg20180208/src/test/test_core_xmpp.py0000644000175500017600000001120113243470025021141 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.test import helpers from constants import Const from twisted.trial import unittest from sat.core import xmpp from twisted.words.protocols.jabber.jid import JID from wokkel.generic import parseXml from wokkel.xmppim import RosterItem class SatXMPPClientTest(unittest.TestCase): def setUp(self): self.host = helpers.FakeSAT() self.client = xmpp.SatXMPPClient(self.host, Const.PROFILE[0], JID("test@example.org"), "test") def test_init(self): """Check that init values are correctly initialised""" self.assertEqual(self.client.profile, Const.PROFILE[0]) print self.client.host self.assertEqual(self.client.host_app, self.host) class SatMessageProtocolTest(unittest.TestCase): def setUp(self): self.host = helpers.FakeSAT() self.message = xmpp.SatMessageProtocol(self.host) self.message.parent = helpers.FakeClient(self.host) def test_onMessage(self): xml = """ test """ stanza = parseXml(xml) self.host.bridge.expectCall("messageNew", u"sender@example.net/house", u"test", u"chat", u"test@example.org/SàT", {}, profile=Const.PROFILE[0]) self.message.onMessage(stanza) class SatRosterProtocolTest(unittest.TestCase): def setUp(self): self.host = helpers.FakeSAT() self.roster = xmpp.SatRosterProtocol(self.host) self.roster.parent = helpers.FakeClient(self.host) def test__registerItem(self): roster_item = RosterItem(Const.JID[0]) roster_item.name = u"Test Man" roster_item.subscriptionTo = True roster_item.subscriptionFrom = True roster_item.ask = False roster_item.groups = set([u"Test Group 1", u"Test Group 2", u"Test Group 3"]) self.host.bridge.expectCall("newContact", Const.JID_STR[0], {'to': 'True', 'from': 'True', 'ask': 'False', 'name': u'Test Man'}, set([u"Test Group 1", u"Test Group 2", u"Test Group 3"]), Const.PROFILE[0]) self.roster._registerItem(roster_item) class SatPresenceProtocolTest(unittest.TestCase): def setUp(self): self.host = helpers.FakeSAT() self.presence = xmpp.SatPresenceProtocol(self.host) self.presence.parent = helpers.FakeClient(self.host) def test_availableReceived(self): self.host.bridge.expectCall("presenceUpdate", Const.JID_STR[0], "xa", 15, {'default': "test status", 'fr': 'statut de test'}, Const.PROFILE[0]) self.presence.availableReceived(Const.JID[0], 'xa', {None: "test status", 'fr': 'statut de test'}, 15) def test_availableReceived_empty_statuses(self): self.host.bridge.expectCall("presenceUpdate", Const.JID_STR[0], "xa", 15, {}, Const.PROFILE[0]) self.presence.availableReceived(Const.JID[0], 'xa', None, 15) def test_unavailableReceived(self): self.host.bridge.expectCall("presenceUpdate", Const.JID_STR[0], "unavailable", 0, {}, Const.PROFILE[0]) self.presence.unavailableReceived(Const.JID[0], None) def test_subscribedReceived(self): self.host.bridge.expectCall("subscribe", "subscribed", Const.JID[0].userhost(), Const.PROFILE[0]) self.presence.subscribedReceived(Const.JID[0]) def test_unsubscribedReceived(self): self.host.bridge.expectCall("subscribe", "unsubscribed", Const.JID[0].userhost(), Const.PROFILE[0]) self.presence.unsubscribedReceived(Const.JID[0]) def test_subscribeReceived(self): self.host.bridge.expectCall("subscribe", "subscribe", Const.JID[0].userhost(), Const.PROFILE[0]) self.presence.subscribeReceived(Const.JID[0]) def test_unsubscribeReceived(self): self.host.bridge.expectCall("subscribe", "unsubscribe", Const.JID[0].userhost(), Const.PROFILE[0]) self.presence.unsubscribeReceived(Const.JID[0]) sat-0.6.1.1+hg20180208/src/test/test_plugin_misc_groupblog.py0000644000175500017600000005014013243470025023543 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin groupblogs """ from constants import Const as C from sat.test import helpers, helpers_plugins from sat.plugins import plugin_misc_groupblog from sat.plugins import plugin_xep_0060 from sat.plugins import plugin_xep_0277 from sat.plugins import plugin_xep_0163 from sat.plugins import plugin_misc_text_syntaxes from twisted.internet import defer from twisted.words.protocols.jabber import jid NS_PUBSUB = 'http://jabber.org/protocol/pubsub' DO_NOT_COUNT_COMMENTS = -1 SERVICE = u'pubsub.example.com' PUBLISHER = u'test@example.org' OTHER_PUBLISHER = u'other@xmpp.net' NODE_ID = u'urn:xmpp:groupblog:{publisher}'.format(publisher=PUBLISHER) OTHER_NODE_ID = u'urn:xmpp:groupblog:{publisher}'.format(publisher=OTHER_PUBLISHER) ITEM_ID_1 = u'c745a688-9b02-11e3-a1a3-c0143dd4fe51' COMMENT_ID_1 = u'd745a688-9b02-11e3-a1a3-c0143dd4fe52' COMMENT_ID_2 = u'e745a688-9b02-11e3-a1a3-c0143dd4fe53' def COMMENTS_NODE_ID(publisher=PUBLISHER): return u'urn:xmpp:comments:_{id}__urn:xmpp:groupblog:{publisher}'.format(id=ITEM_ID_1, publisher=publisher) def COMMENTS_NODE_URL(publisher=PUBLISHER): return u'xmpp:{service}?node={node}'.format(service=SERVICE, id=ITEM_ID_1, node=COMMENTS_NODE_ID(publisher).replace(':', '%3A').replace('@', '%40')) def ITEM(publisher=PUBLISHER): return u""" The Uses of This World {id} 2003-12-12T17:47:23Z 2003-12-12T17:47:23Z {publisher} """.format(ns=NS_PUBSUB, id=ITEM_ID_1, publisher=publisher, comments_node_url=COMMENTS_NODE_URL(publisher)) def COMMENT(id_=COMMENT_ID_1): return u""" The Uses of This World {id} 2003-12-12T17:47:23Z 2003-12-12T17:47:23Z {publisher} """.format(ns=NS_PUBSUB, id=id_, publisher=PUBLISHER) def ITEM_DATA(id_=ITEM_ID_1, count=0): res = {'id': ITEM_ID_1, 'type': 'main_item', 'content': 'The Uses of This World', 'author': PUBLISHER, 'updated': '1071251243.0', 'published': '1071251243.0', 'service': SERVICE, 'comments': COMMENTS_NODE_URL_1, 'comments_service': SERVICE, 'comments_node': COMMENTS_NODE_ID_1} if count != DO_NOT_COUNT_COMMENTS: res.update({'comments_count': unicode(count)}) return res def COMMENT_DATA(id_=COMMENT_ID_1): return {'id': id_, 'type': 'comment', 'content': 'The Uses of This World', 'author': PUBLISHER, 'updated': '1071251243.0', 'published': '1071251243.0', 'service': SERVICE, 'node': COMMENTS_NODE_ID_1, 'verified_publisher': 'false'} COMMENTS_NODE_ID_1 = COMMENTS_NODE_ID() COMMENTS_NODE_ID_2 = COMMENTS_NODE_ID(OTHER_PUBLISHER) COMMENTS_NODE_URL_1 = COMMENTS_NODE_URL() COMMENTS_NODE_URL_2 = COMMENTS_NODE_URL(OTHER_PUBLISHER) ITEM_1 = ITEM() ITEM_2 = ITEM(OTHER_PUBLISHER) COMMENT_1 = COMMENT(COMMENT_ID_1) COMMENT_2 = COMMENT(COMMENT_ID_2) def ITEM_DATA_1(count=0): return ITEM_DATA(count=count) COMMENT_DATA_1 = COMMENT_DATA() COMMENT_DATA_2 = COMMENT_DATA(COMMENT_ID_2) class XEP_groupblogTest(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.host.plugins['XEP-0060'] = plugin_xep_0060.XEP_0060(self.host) self.host.plugins['XEP-0163'] = plugin_xep_0163.XEP_0163(self.host) reload(plugin_misc_text_syntaxes) # reload the plugin to avoid conflict error self.host.plugins['TEXT-SYNTAXES'] = plugin_misc_text_syntaxes.TextSyntaxes(self.host) self.host.plugins['XEP-0277'] = plugin_xep_0277.XEP_0277(self.host) self.plugin = plugin_misc_groupblog.GroupBlog(self.host) self.plugin._initialise = self._initialise self.__initialised = False self._initialise(C.PROFILE[0]) def _initialise(self, profile_key): profile = profile_key client = self.host.getClient(profile) if not self.__initialised: client.item_access_pubsub = jid.JID(SERVICE) xep_0060 = self.host.plugins['XEP-0060'] client.pubsub_client = helpers_plugins.FakeSatPubSubClient(self.host, xep_0060) client.pubsub_client.parent = client self.psclient = client.pubsub_client helpers.FakeSAT.getDiscoItems = self.psclient.service_getDiscoItems self.__initialised = True return defer.succeed((profile, client)) def _addItem(self, profile, item, parent_node=None): client = self.host.getClient(profile) client.pubsub_client._addItem(item, parent_node) def test_sendGroupBlog(self): self._initialise(C.PROFILE[0]) d = self.psclient.items(SERVICE, NODE_ID) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.plugin.sendGroupBlog('PUBLIC', [], 'test', {}, C.PROFILE[0])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) return d.addCallback(lambda items: self.assertEqual(len(items), 1)) def test_deleteGroupBlog(self): pub_data = (SERVICE, NODE_ID, ITEM_ID_1) self.host.bridge.expectCall('personalEvent', C.JID_STR[0], "MICROBLOG_DELETE", {'type': 'main_item', 'id': ITEM_ID_1}, C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.deleteGroupBlog(pub_data, COMMENTS_NODE_URL_1, profile_key=C.PROFILE[0])) return d.addCallback(self.assertEqual, None) def test_updateGroupBlog(self): pub_data = (SERVICE, NODE_ID, ITEM_ID_1) new_text = u"silfu23RFWUP)IWNOEIOEFÖ" self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.updateGroupBlog(pub_data, COMMENTS_NODE_URL_1, new_text, {}, profile_key=C.PROFILE[0])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) return d.addCallback(lambda items: self.assertEqual(''.join(items[0].entry.title.children), new_text)) def test_sendGroupBlogComment(self): self._initialise(C.PROFILE[0]) d = self.psclient.items(SERVICE, NODE_ID) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.plugin.sendGroupBlogComment(COMMENTS_NODE_URL_1, 'test', {}, profile_key=C.PROFILE[0])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) return d.addCallback(lambda items: self.assertEqual(len(items), 1)) def test_getGroupBlogs(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getGroupBlogs(PUBLISHER, profile_key=C.PROFILE[0])) result = ([ITEM_DATA_1()], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsNoCount(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getGroupBlogs(PUBLISHER, count_comments=False, profile_key=C.PROFILE[0])) result = ([ITEM_DATA_1(DO_NOT_COUNT_COMMENTS)], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsWithIDs(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getGroupBlogs(PUBLISHER, [ITEM_ID_1], profile_key=C.PROFILE[0])) result = ([ITEM_DATA_1()], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsWithRSM(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getGroupBlogs(PUBLISHER, rsm_data={'max_': 1}, profile_key=C.PROFILE[0])) result = ([ITEM_DATA_1()], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsWithComments(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1])) d.addCallback(lambda dummy: self.plugin.getGroupBlogsWithComments(PUBLISHER, [], profile_key=C.PROFILE[0])) result = ([(ITEM_DATA_1(1), ([COMMENT_DATA_1], {'count': '1', 'index': '0', 'first': COMMENT_ID_1, 'last': COMMENT_ID_1}))], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsWithComments2(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.plugin.getGroupBlogsWithComments(PUBLISHER, [], profile_key=C.PROFILE[0])) result = ([(ITEM_DATA_1(2), ([COMMENT_DATA_1, COMMENT_DATA_2], {'count': '2', 'index': '0', 'first': COMMENT_ID_1, 'last': COMMENT_ID_2}))], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1}) return d.addCallback(self.assertEqual, result) def test_getGroupBlogsAtom(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getGroupBlogsAtom(PUBLISHER, {'max_': 1}, profile_key=C.PROFILE[0])) def cb(atom): self.assertIsInstance(atom, unicode) self.assertTrue(atom.startswith('')) return d.addCallback(cb) def test_getMassiveGroupBlogs(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.plugin.getMassiveGroupBlogs('JID', [jid.JID(PUBLISHER)], {'max_': 1}, profile_key=C.PROFILE[0])) result = {PUBLISHER: ([ITEM_DATA_1()], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1})} def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] return res d.addCallback(clean) d.addCallback(self.assertEqual, result) def test_getMassiveGroupBlogsWithComments(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.plugin.getMassiveGroupBlogs('JID', [jid.JID(PUBLISHER)], {'max_': 1}, profile_key=C.PROFILE[0])) result = {PUBLISHER: ([ITEM_DATA_1(2)], {'count': '1', 'index': '0', 'first': ITEM_ID_1, 'last': ITEM_ID_1})} def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] return res d.addCallback(clean) d.addCallback(self.assertEqual, result) def test_getGroupBlogComments(self): self._initialise(C.PROFILE[0]) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1])) d.addCallback(lambda dummy: self.plugin.getGroupBlogComments(SERVICE, COMMENTS_NODE_ID_1, {'max_': 1}, profile_key=C.PROFILE[0])) result = ([COMMENT_DATA_1], {'count': '1', 'index': '0', 'first': COMMENT_ID_1, 'last': COMMENT_ID_1}) return d.addCallback(self.assertEqual, result) def test_subscribeGroupBlog(self): self._initialise(C.PROFILE[0]) d = self.plugin.subscribeGroupBlog(PUBLISHER, profile_key=C.PROFILE[0]) return d.addCallback(self.assertEqual, None) def test_massiveSubscribeGroupBlogs(self): self._initialise(C.PROFILE[0]) d = self.plugin.massiveSubscribeGroupBlogs('JID', [jid.JID(PUBLISHER)], profile_key=C.PROFILE[0]) def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@subscriptions@' + SERVICE] return res d.addCallback(clean) return d.addCallback(self.assertEqual, None) def test_deleteAllGroupBlogs(self): """Delete our main node and associated comments node""" self._initialise(C.PROFILE[0]) self.host.profiles[C.PROFILE[0]].roster.addItem(jid.JID(OTHER_PUBLISHER)) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, OTHER_NODE_ID, [ITEM_2])) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_2, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] return res d.addCallback(lambda dummy: self.plugin.deleteAllGroupBlogs(C.PROFILE[0])) d.addCallback(clean) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) return d def test_deleteAllGroupBlogsComments(self): """Delete the comments we posted on other node's""" self._initialise(C.PROFILE[0]) self.host.profiles[C.PROFILE[0]].roster.addItem(jid.JID(OTHER_PUBLISHER)) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, OTHER_NODE_ID, [ITEM_2])) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_2, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] return res d.addCallback(lambda dummy: self.plugin.deleteAllGroupBlogsComments(C.PROFILE[0])) d.addCallback(clean) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) return d def test_deleteAllGroupBlogsAndComments(self): self._initialise(C.PROFILE[0]) self.host.profiles[C.PROFILE[0]].roster.addItem(jid.JID(OTHER_PUBLISHER)) d = self.psclient.publish(SERVICE, NODE_ID, [ITEM_1]) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_1, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, OTHER_NODE_ID, [ITEM_2])) d.addCallback(lambda dummy: self.psclient.publish(SERVICE, COMMENTS_NODE_ID_2, [COMMENT_1, COMMENT_2])) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 2)) def clean(res): del self.host.plugins['XEP-0060'].node_cache[C.PROFILE[0] + '@found@' + SERVICE] return res d.addCallback(lambda dummy: self.plugin.deleteAllGroupBlogsAndComments(C.PROFILE[0])) d.addCallback(clean) d.addCallback(lambda dummy: self.psclient.items(SERVICE, NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_1)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, OTHER_NODE_ID)) d.addCallback(lambda items: self.assertEqual(len(items), 1)) d.addCallback(lambda dummy: self.psclient.items(SERVICE, COMMENTS_NODE_ID_2)) d.addCallback(lambda items: self.assertEqual(len(items), 0)) return d sat-0.6.1.1+hg20180208/src/test/helpers_plugins.py0000644000175500017600000002447613243470025021333 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Helpers class for plugin dependencies """ from twisted.internet import defer from wokkel.muc import Room, User from wokkel.generic import parseXml from wokkel.disco import DiscoItem, DiscoItems # temporary until the changes are integrated to Wokkel from sat_tmp.wokkel.rsm import RSMResponse from constants import Const as C from sat.plugins import plugin_xep_0045 from collections import OrderedDict class FakeMUCClient(object): def __init__(self, plugin_parent): self.plugin_parent = plugin_parent self.host = plugin_parent.host self.joined_rooms = {} def join(self, room_jid, nick, options=None, profile_key=C.PROF_KEY_NONE): """ @param room_jid: the room JID @param nick: nick to be used in the room @param options: joining options @param profile_key: the profile key of the user joining the room @return: the deferred joined wokkel.muc.Room instance """ profile = self.host.memory.getProfileName(profile_key) roster = {} # ask the other profiles to fill our roster for i in xrange(0, len(C.PROFILE)): other_profile = C.PROFILE[i] if other_profile == profile: continue try: other_room = self.plugin_parent.clients[other_profile].joined_rooms[room_jid] roster.setdefault(other_room.nick, User(other_room.nick, C.PROFILE_DICT[other_profile])) for other_nick in other_room.roster: roster.setdefault(other_nick, other_room.roster[other_nick]) except (AttributeError, KeyError): pass # rename our nick if it already exists while nick in roster.keys(): if C.PROFILE_DICT[profile].userhost() == roster[nick].entity.userhost(): break # same user with different resource --> same nickname nick = nick + "_" room = Room(room_jid, nick) room.roster = roster self.joined_rooms[room_jid] = room # fill the other rosters with the new entry for i in xrange(0, len(C.PROFILE)): other_profile = C.PROFILE[i] if other_profile == profile: continue try: other_room = self.plugin_parent.clients[other_profile].joined_rooms[room_jid] other_room.roster.setdefault(room.nick, User(room.nick, C.PROFILE_DICT[profile])) except (AttributeError, KeyError): pass return defer.succeed(room) def leave(self, roomJID, profile_key=C.PROF_KEY_NONE): """ @param roomJID: the room JID @param profile_key: the profile key of the user joining the room @return: a dummy deferred """ profile = self.host.memory.getProfileName(profile_key) room = self.joined_rooms[roomJID] # remove ourself from the other rosters for i in xrange(0, len(C.PROFILE)): other_profile = C.PROFILE[i] if other_profile == profile: continue try: other_room = self.plugin_parent.clients[other_profile].joined_rooms[roomJID] del other_room.roster[room.nick] except (AttributeError, KeyError): pass del self.joined_rooms[roomJID] return defer.Deferred() class FakeXEP_0045(plugin_xep_0045.XEP_0045): def __init__(self, host): self.host = host self.clients = {} for profile in C.PROFILE: self.clients[profile] = FakeMUCClient(self) def join(self, room_jid, nick, options={}, profile_key='@DEFAULT@'): """ @param roomJID: the room JID @param nick: nick to be used in the room @param options: ignore @param profile_key: the profile of the user joining the room @return: the deferred joined wokkel.muc.Room instance or None """ profile = self.host.memory.getProfileName(profile_key) if room_jid in self.clients[profile].joined_rooms: return defer.succeed(None) room = self.clients[profile].join(room_jid, nick, profile_key=profile) return room def joinRoom(self, muc_index, user_index): """Called by tests @return: the nickname of the user who joined room""" muc_jid = C.MUC[muc_index] nick = C.JID[user_index].user profile = C.PROFILE[user_index] self.join(muc_jid, nick, profile_key=profile) return self.getNick(muc_index, user_index) def leave(self, room_jid, profile_key='@DEFAULT@'): """ @param roomJID: the room JID @param profile_key: the profile of the user leaving the room @return: a dummy deferred """ profile = self.host.memory.getProfileName(profile_key) if room_jid not in self.clients[profile].joined_rooms: raise plugin_xep_0045.UnknownRoom("This room has not been joined") return self.clients[profile].leave(room_jid, profile) def leaveRoom(self, muc_index, user_index): """Called by tests @return: the nickname of the user who left the room""" muc_jid = C.MUC[muc_index] nick = self.getNick(muc_index, user_index) profile = C.PROFILE[user_index] self.leave(muc_jid, profile_key=profile) return nick def getRoom(self, muc_index, user_index): """Called by tests @return: a wokkel.muc.Room instance""" profile = C.PROFILE[user_index] muc_jid = C.MUC[muc_index] try: return self.clients[profile].joined_rooms[muc_jid] except (AttributeError, KeyError): return None def getNick(self, muc_index, user_index): try: return self.getRoomNick(C.MUC[muc_index], C.PROFILE[user_index]) except (KeyError, AttributeError): return '' def getNickOfUser(self, muc_index, user_index, profile_index, secure=True): try: room = self.clients[C.PROFILE[profile_index]].joined_rooms[C.MUC[muc_index]] return self.getRoomNickOfUser(room, C.JID[user_index]) except (KeyError, AttributeError): return None class FakeXEP_0249(object): def __init__(self, host): self.host = host def invite(self, target, room, options={}, profile_key='@DEFAULT@'): """ Invite a user to a room. To accept the invitation from a test, just call FakeXEP_0045.joinRoom (no need to have a dedicated method). @param target: jid of the user to invite @param room: jid of the room where the user is invited @options: attribute with extra info (reason, password) as in #XEP-0249 @profile_key: %(doc_profile_key)s """ pass class FakeSatPubSubClient(object): def __init__(self, host, parent_plugin): self.host = host self.parent_plugin = parent_plugin self.__items = OrderedDict() self.__rsm_responses = {} def createNode(self, service, nodeIdentifier=None, options=None, sender=None): return defer.succeed(None) def deleteNode(self, service, nodeIdentifier, sender=None): try: del self.__items[nodeIdentifier] except KeyError: pass return defer.succeed(None) def subscribe(self, service, nodeIdentifier, subscriber, options=None, sender=None): return defer.succeed(None) def unsubscribe(self, service, nodeIdentifier, subscriber, subscriptionIdentifier=None, sender=None): return defer.succeed(None) def publish(self, service, nodeIdentifier, items=None, sender=None): node = self.__items.setdefault(nodeIdentifier, []) def replace(item_obj): index = 0 for current in node: if current['id'] == item_obj['id']: node[index] = item_obj return True index += 1 return False for item in items: item_obj = parseXml(item) if isinstance(item, unicode) else item if not replace(item_obj): node.append(item_obj) return defer.succeed(None) def items(self, service, nodeIdentifier, maxItems=None, itemIdentifiers=None, subscriptionIdentifier=None, sender=None, ext_data=None): try: items = self.__items[nodeIdentifier] except KeyError: items = [] if ext_data: assert('id' in ext_data) if 'rsm' in ext_data: args = (0, items[0]['id'], items[-1]['id']) if items else () self.__rsm_responses[ext_data['id']] = RSMResponse(len(items), *args) return defer.succeed(items) def retractItems(self, service, nodeIdentifier, itemIdentifiers, sender=None): node = self.__items[nodeIdentifier] for item in [item for item in node if item['id'] in itemIdentifiers]: node.remove(item) return defer.succeed(None) def getRSMResponse(self, id): if id not in self.__rsm_responses: return {} result = self.__rsm_responses[id].toDict() del self.__rsm_responses[id] return result def subscriptions(self, service, nodeIdentifier, sender=None): return defer.succeed([]) def service_getDiscoItems(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): items = DiscoItems() for item in self.__items.keys(): items.append(DiscoItem(service, item)) return defer.succeed(items) sat-0.6.1.1+hg20180208/src/test/__init__.py0000644000175500017600000000000013243470025017640 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/test/test_plugin_misc_text_syntaxes.py0000644000175500017600000001220713243470025024467 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin text syntaxes tests """ from sat.test import helpers from sat.plugins import plugin_misc_text_syntaxes from twisted.trial.unittest import SkipTest import re class SanitisationTest(helpers.SatTestCase): EVIL_HTML1 = """ a link another link

a paragraph

secret EVIL!
of EVIL!
Password:
annoying EVIL! spam spam SPAM! """ # example from lxml: /usr/share/doc/python-lxml-doc/html/lxmlhtml.html#cleaning-up-html EVIL_HTML2 = """

test retest
toto

""" def setUp(self): self.host = helpers.FakeSAT() reload(plugin_misc_text_syntaxes) # reload the plugin to avoid conflict error self.text_syntaxes = plugin_misc_text_syntaxes.TextSyntaxes(self.host) def test_xhtml_sanitise(self): expected = u"""
a link another link

a paragraph

secret EVIL!
of EVIL! Password: annoying EVIL! spam spam SPAM!
""" d = self.text_syntaxes.cleanXHTML(self.EVIL_HTML1) d.addCallback(self.assertEqualXML, expected, ignore_blank=True) return d def test_styles_sanitise(self): expected = u"""

test retest
toto

""" d = self.text_syntaxes.cleanXHTML(self.EVIL_HTML2) d.addCallback(self.assertEqualXML, expected) return d def test_html2text(self): """Check that html2text is not inserting \n in the middle of that link. By default lines are truncated after the 79th characters.""" source = "\"sat\"/" expected = "![sat](http://sat.goffi.org/static/images/screenshots/libervia/libervia_discussions.png)" try: d = self.text_syntaxes.convert(source, self.text_syntaxes.SYNTAX_XHTML, self.text_syntaxes.SYNTAX_MARKDOWN) except plugin_misc_text_syntaxes.UnknownSyntax: raise SkipTest("Markdown syntax is not available.") d.addCallback(self.assertEqual, expected) return d def test_removeXHTMLMarkups(self): expected = u""" a link another link a paragraph secret EVIL! of EVIL! Password: annoying EVIL! spam spam SPAM! """ result = self.text_syntaxes._removeMarkups(self.EVIL_HTML1) self.assertEqual(re.sub(r"\s+", " ", result).rstrip(), expected.rstrip()) expected = u"""test retest toto""" result = self.text_syntaxes._removeMarkups(self.EVIL_HTML2) self.assertEqual(re.sub(r"\s+", " ", result).rstrip(), expected.rstrip()) sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0085.py0000644000175500017600000000670013243470025022003 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin chat states notification tests """ from constants import Const from sat.test import helpers from sat.core.constants import Const as C from sat.plugins import plugin_xep_0085 as plugin from copy import deepcopy from twisted.internet import defer from wokkel.generic import parseXml class XEP_0085Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = plugin.XEP_0085(self.host) self.host.memory.setParam(plugin.PARAM_NAME, True, plugin.PARAM_KEY, C.NO_SECURITY_LIMIT, Const.PROFILE[0]) def test_messageReceived(self): for state in plugin.CHAT_STATES: xml = u""" %s <%s xmlns='%s'/> """ % (Const.JID_STR[1], Const.JID_STR[0], "test" if state == "active" else "", state, plugin.NS_CHAT_STATES) stanza = parseXml(xml.encode("utf-8")) self.host.bridge.expectCall("chatStateReceived", Const.JID_STR[1], state, Const.PROFILE[0]) self.plugin.messageReceivedTrigger(self.host.getClient(Const.PROFILE[0]), stanza, None) def test_messageSendTrigger(self): def cb(data): xml = data['xml'].toXml().encode("utf-8") self.assertEqualXML(xml, expected.toXml().encode("utf-8")) d_list = [] for state in plugin.CHAT_STATES: mess_data = {"to": Const.JID[0], "type": "chat", "message": "content", "extra": {} if state == "active" else {"chat_state": state}} stanza = u""" %s """ % (Const.JID_STR[1], Const.JID_STR[0], ("%s" % mess_data['message']) if state == "active" else "") mess_data['xml'] = parseXml(stanza.encode("utf-8")) expected = deepcopy(mess_data['xml']) expected.addElement(state, plugin.NS_CHAT_STATES) post_treatments = defer.Deferred() self.plugin.messageSendTrigger(self.host.getClient(Const.PROFILE[0]), mess_data, None, post_treatments) post_treatments.addCallback(cb) post_treatments.callback(mess_data) d_list.append(post_treatments) def cb_list(dummy): # cancel the timer to not block the process self.plugin.map[Const.PROFILE[0]][Const.JID[0]].timer.cancel() return defer.DeferredList(d_list).addCallback(cb_list) sat-0.6.1.1+hg20180208/src/test/test_helpers_plugins.py0000644000175500017600000001531213243470025022357 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Test the helper classes to see if they behave well""" from sat.test import helpers from sat.test import helpers_plugins class FakeXEP_0045Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = helpers_plugins.FakeXEP_0045(self.host) def test_joinRoom(self): self.plugin.joinRoom(0, 0) self.assertEqual('test', self.plugin.getNick(0, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 0)) self.assertEqual('', self.plugin.getNick(0, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 1)) self.assertEqual('', self.plugin.getNick(0, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 2)) self.assertEqual('', self.plugin.getNick(0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 3)) self.plugin.joinRoom(0, 1) self.assertEqual('test', self.plugin.getNick(0, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 0)) self.assertEqual('sender', self.plugin.getNick(0, 1)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 1)) self.assertEqual('', self.plugin.getNick(0, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 2)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 2)) self.assertEqual('', self.plugin.getNick(0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 3)) self.plugin.joinRoom(0, 2) self.assertEqual('test', self.plugin.getNick(0, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 0)) self.assertEqual('sender', self.plugin.getNick(0, 1)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 1)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 1)) # Const.JID[2] is in the roster for Const.PROFILE[1] self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 1)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 1)) self.assertEqual('sender', self.plugin.getNick(0, 2)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 2)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 2)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 2)) # Const.JID[1] is in the roster for Const.PROFILE[2] self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 2)) self.assertEqual('', self.plugin.getNick(0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 1, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 2, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 3)) self.plugin.joinRoom(0, 3) self.assertEqual('test', self.plugin.getNick(0, 0)) self.assertEqual(None, self.plugin.getNickOfUser(0, 0, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 0)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 0)) self.assertEqual('sender_', self.plugin.getNickOfUser(0, 3, 0)) self.assertEqual('sender', self.plugin.getNick(0, 1)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 1)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 1)) # Const.JID[2] is in the roster for Const.PROFILE[1] self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 1)) self.assertEqual('sender_', self.plugin.getNickOfUser(0, 3, 1)) self.assertEqual('sender', self.plugin.getNick(0, 2)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 2)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 2)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 2)) # Const.JID[1] is in the roster for Const.PROFILE[2] self.assertEqual('sender_', self.plugin.getNickOfUser(0, 3, 2)) self.assertEqual('sender_', self.plugin.getNick(0, 3)) self.assertEqual('test', self.plugin.getNickOfUser(0, 0, 3)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 1, 3)) self.assertEqual('sender', self.plugin.getNickOfUser(0, 2, 3)) self.assertEqual(None, self.plugin.getNickOfUser(0, 3, 3)) sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0313.py0000644000175500017600000002377213243470025022005 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin XEP-0313 """ from constants import Const as C from sat.test import helpers from sat.plugins.plugin_xep_0313 import XEP_0313 from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish from wokkel.data_form import Field from dateutil.tz import tzutc import datetime # TODO: change this when RSM and MAM are in wokkel from sat_tmp.wokkel.rsm import RSMRequest from sat_tmp.wokkel.mam import buildForm, MAMRequest NS_PUBSUB = 'http://jabber.org/protocol/pubsub' SERVICE = 'sat-pubsub.tazar.int' SERVICE_JID = JID(SERVICE) class XEP_0313Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = XEP_0313(self.host) self.client = self.host.getClient(C.PROFILE[0]) mam_client = self.plugin.getHandler(C.PROFILE[0]) mam_client.makeConnection(self.host.getClient(C.PROFILE[0]).xmlstream) def test_queryArchive(self): xml = """ """ % (("H_%d" % domish.Element._idCounter), SERVICE) d = self.plugin.queryArchive(self.client, MAMRequest(), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchivePubsub(self): xml = """ """ % (("H_%d" % domish.Element._idCounter), SERVICE) d = self.plugin.queryArchive(self.client, MAMRequest(node="fdp/submitted/capulet.lit/sonnets"), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveWith(self): xml = """ urn:xmpp:mam:1 juliet@capulet.lit """ % (("H_%d" % domish.Element._idCounter), SERVICE) form = buildForm(with_jid=JID('juliet@capulet.lit')) d = self.plugin.queryArchive(self.client, MAMRequest(form), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveStartEnd(self): xml = """ urn:xmpp:mam:1 2010-06-07T00:00:00Z 2010-07-07T13:23:54Z """ % (("H_%d" % domish.Element._idCounter), SERVICE) start = datetime.datetime(2010, 6, 7, 0, 0, 0, tzinfo=tzutc()) end = datetime.datetime(2010, 7, 7, 13, 23, 54, tzinfo=tzutc()) form = buildForm(start=start, end=end) d = self.plugin.queryArchive(self.client, MAMRequest(form), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveStart(self): xml = """ urn:xmpp:mam:1 2010-08-07T00:00:00Z """ % (("H_%d" % domish.Element._idCounter), SERVICE) start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc()) form = buildForm(start=start) d = self.plugin.queryArchive(self.client, MAMRequest(form), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveRSM(self): xml = """ urn:xmpp:mam:1 2010-08-07T00:00:00Z 10 """ % (("H_%d" % domish.Element._idCounter), SERVICE) start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc()) form = buildForm(start=start) rsm = RSMRequest(max_=10) d = self.plugin.queryArchive(self.client, MAMRequest(form, rsm), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveRSMPaging(self): xml = """ urn:xmpp:mam:1 2010-08-07T00:00:00Z 10 09af3-cc343-b409f """ % (("H_%d" % domish.Element._idCounter), SERVICE) start = datetime.datetime(2010, 8, 7, 0, 0, 0, tzinfo=tzutc()) form = buildForm(start=start) rsm = RSMRequest(max_=10, after=u'09af3-cc343-b409f') d = self.plugin.queryArchive(self.client, MAMRequest(form, rsm), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryFields(self): xml = """ """ % (("H_%d" % domish.Element._idCounter), SERVICE) d = self.plugin.queryFields(self.client, SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryArchiveFields(self): xml = """ urn:xmpp:mam:1 Where arth thou, my Juliet? {http://jabber.org/protocol/mood}mood/lonely """ % (("H_%d" % domish.Element._idCounter), SERVICE) extra_fields = [Field('text-single', 'urn:example:xmpp:free-text-search', 'Where arth thou, my Juliet?'), Field('text-single', 'urn:example:xmpp:stanza-content', '{http://jabber.org/protocol/mood}mood/lonely') ] form = buildForm(extra_fields=extra_fields) d = self.plugin.queryArchive(self.client, MAMRequest(form), SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_queryPrefs(self): xml = """ """ % (("H_%d" % domish.Element._idCounter), SERVICE) d = self.plugin.getPrefs(self.client, SERVICE_JID) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d def test_setPrefs(self): xml = """ romeo@montague.lit montague@montague.lit """ % (("H_%d" % domish.Element._idCounter), SERVICE) always = [JID('romeo@montague.lit')] never = [JID('montague@montague.lit')] d = self.plugin.setPrefs(self.client, SERVICE_JID, always=always, never=never) d.addCallback(lambda dummy: self.assertEqualXML(self.host.getSentMessageXml(0), xml, True)) return d sat-0.6.1.1+hg20180208/src/test/test_plugin_misc_room_game.py0000644000175500017600000006113613243470025023517 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Tests for the plugin room game (base class for MUC games) """ from sat.core.i18n import _ from constants import Const from sat.test import helpers, helpers_plugins from sat.plugins import plugin_misc_room_game as plugin from twisted.words.protocols.jabber.jid import JID from wokkel.muc import User from logging import WARNING # Data used for test initialization NAMESERVICE = 'http://www.goffi.org/protocol/dummy' TAG = 'dummy' PLUGIN_INFO = { "name": "Dummy plugin", "import_name": "DUMMY", "type": "MISC", "protocols": [], "dependencies": [], "main": "Dummy", "handler": "no", # handler MUST be "no" (dynamic inheritance) "description": _("""Dummy plugin to test room game""") } ROOM_JID = JID(Const.MUC_STR[0]) PROFILE = Const.PROFILE[0] OTHER_PROFILE = Const.PROFILE[1] class RoomGameTest(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() def reinit(self, game_init={}, player_init={}): self.host.reinit() self.plugin = plugin.RoomGame(self.host) self.plugin._init_(self.host, PLUGIN_INFO, (NAMESERVICE, TAG), game_init, player_init) self.plugin_0045 = self.host.plugins['XEP-0045'] = helpers_plugins.FakeXEP_0045(self.host) self.plugin_0249 = self.host.plugins['XEP-0249'] = helpers_plugins.FakeXEP_0249(self.host) for profile in Const.PROFILE: self.host.getClient(profile) # init self.host.profiles[profile] def initGame(self, muc_index, user_index): self.plugin_0045.joinRoom(user_index, muc_index) self.plugin._initGame(JID(Const.MUC_STR[muc_index]), Const.JID[user_index].user) def _expectedMessage(self, to, type_, tag, players=[]): content = "<%s" % tag if not players: content += "/>" else: content += ">" for i in xrange(0, len(players)): content += "%s" % (i, players[i]) content += "" % tag return "<%s xmlns='%s'>%s" % (to.full(), type_, TAG, NAMESERVICE, content) def test_createOrInvite_solo(self): self.reinit() self.plugin_0045.joinRoom(0, 0) self.plugin._createOrInvite(self.plugin_0045.getRoom(0, 0), [], Const.PROFILE[0]) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) def test_createOrInvite_multi_not_waiting(self): self.reinit() self.plugin_0045.joinRoom(0, 0) other_players = [Const.JID[1], Const.JID[2]] self.plugin._createOrInvite(self.plugin_0045.getRoom(0, 0), other_players, Const.PROFILE[0]) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) def test_createOrInvite_multi_waiting(self): self.reinit(player_init={'score': 0}) self.plugin_0045.joinRoom(0, 0) other_players = [Const.JID[1], Const.JID[2]] self.plugin._createOrInvite(self.plugin_0045.getRoom(0, 0), other_players, Const.PROFILE[0]) self.assertTrue(self.plugin._gameExists(ROOM_JID, False)) self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) def test_initGame(self): self.reinit() self.initGame(0, 0) self.assertTrue(self.plugin.isReferee(ROOM_JID, Const.JID[0].user)) self.assertEqual([], self.plugin.games[ROOM_JID]['players']) def test_checkJoinAuth(self): self.reinit() check = lambda value: getattr(self, "assert%s" % value)(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[0], Const.JID[0].user)) check(False) # to test the "invited" mode, the referee must be different than the user to test self.initGame(0, 1) self.plugin.join_mode = self.plugin.ALL check(True) self.plugin.join_mode = self.plugin.INVITED check(False) self.plugin.invitations[ROOM_JID] = [(None, [Const.JID[0].userhostJID()])] check(True) self.plugin.join_mode = self.plugin.NONE check(False) self.plugin.games[ROOM_JID]['players'].append(Const.JID[0].user) check(True) def test_updatePlayers(self): self.reinit() self.initGame(0, 0) self.assertEqual(self.plugin.games[ROOM_JID]['players'], []) self.plugin._updatePlayers(ROOM_JID, [], True, Const.PROFILE[0]) self.assertEqual(self.plugin.games[ROOM_JID]['players'], []) self.plugin._updatePlayers(ROOM_JID, ["user1"], True, Const.PROFILE[0]) self.assertEqual(self.plugin.games[ROOM_JID]['players'], ["user1"]) self.plugin._updatePlayers(ROOM_JID, ["user2", "user3"], True, Const.PROFILE[0]) self.assertEqual(self.plugin.games[ROOM_JID]['players'], ["user1", "user2", "user3"]) self.plugin._updatePlayers(ROOM_JID, ["user2", "user3"], True, Const.PROFILE[0]) # should not be stored twice self.assertEqual(self.plugin.games[ROOM_JID]['players'], ["user1", "user2", "user3"]) def test_synchronizeRoom(self): self.reinit() self.initGame(0, 0) self.plugin._synchronizeRoom(ROOM_JID, [Const.MUC[0]], Const.PROFILE[0]) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "players", [])) self.plugin.games[ROOM_JID]['players'].append("test1") self.plugin._synchronizeRoom(ROOM_JID, [Const.MUC[0]], Const.PROFILE[0]) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "players", ["test1"])) self.plugin.games[ROOM_JID]['started'] = True self.plugin.games[ROOM_JID]['players'].append("test2") self.plugin._synchronizeRoom(ROOM_JID, [Const.MUC[0]], Const.PROFILE[0]) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "started", ["test1", "test2"])) self.plugin.games[ROOM_JID]['players'].append("test3") self.plugin.games[ROOM_JID]['players'].append("test4") user1 = JID(ROOM_JID.userhost() + "/" + Const.JID[0].user) user2 = JID(ROOM_JID.userhost() + "/" + Const.JID[1].user) self.plugin._synchronizeRoom(ROOM_JID, [user1, user2], Const.PROFILE[0]) self.assertEqualXML(self.host.getSentMessageXml(0), self._expectedMessage(user1, "normal", "started", ["test1", "test2", "test3", "test4"])) self.assertEqualXML(self.host.getSentMessageXml(0), self._expectedMessage(user2, "normal", "started", ["test1", "test2", "test3", "test4"])) def test_invitePlayers(self): self.reinit() self.initGame(0, 0) self.plugin_0045.joinRoom(0, 1) self.assertEqual(self.plugin.invitations[ROOM_JID], []) room = self.plugin_0045.getRoom(0, 0) nicks = self.plugin._invitePlayers(room, [Const.JID[1], Const.JID[2]], Const.JID[0].user, Const.PROFILE[0]) self.assertEqual(self.plugin.invitations[ROOM_JID][0][1], [Const.JID[1].userhostJID(), Const.JID[2].userhostJID()]) # the following assertion is True because Const.JID[1] and Const.JID[2] have the same userhost self.assertEqual(nicks, [Const.JID[1].user, Const.JID[2].user]) nicks = self.plugin._invitePlayers(room, [Const.JID[1], Const.JID[3]], Const.JID[0].user, Const.PROFILE[0]) self.assertEqual(self.plugin.invitations[ROOM_JID][1][1], [Const.JID[1].userhostJID(), Const.JID[3].userhostJID()]) # this time Const.JID[1] and Const.JID[3] have the same user but the host differs self.assertEqual(nicks, [Const.JID[1].user]) def test_checkInviteAuth(self): def check(value, index): nick = self.plugin_0045.getNick(0, index) getattr(self, "assert%s" % value)(self.plugin._checkInviteAuth(ROOM_JID, nick)) self.reinit() for mode in [self.plugin.FROM_ALL, self.plugin.FROM_NONE, self.plugin.FROM_REFEREE, self.plugin.FROM_PLAYERS]: self.plugin.invite_mode = mode check(True, 0) self.initGame(0, 0) self.plugin.invite_mode = self.plugin.FROM_ALL check(True, 0) check(True, 1) self.plugin.invite_mode = self.plugin.FROM_NONE check(True, 0) # game initialized but not started yet, referee can invite check(False, 1) self.plugin.invite_mode = self.plugin.FROM_REFEREE check(True, 0) check(False, 1) user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.games[ROOM_JID]['players'].append(user_nick) self.plugin.invite_mode = self.plugin.FROM_PLAYERS check(True, 0) check(True, 1) check(False, 2) def test_isReferee(self): self.reinit() self.initGame(0, 0) self.assertTrue(self.plugin.isReferee(ROOM_JID, self.plugin_0045.getNick(0, 0))) self.assertFalse(self.plugin.isReferee(ROOM_JID, self.plugin_0045.getNick(0, 1))) def test_isPlayer(self): self.reinit() self.initGame(0, 0) self.assertTrue(self.plugin.isPlayer(ROOM_JID, self.plugin_0045.getNick(0, 0))) user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.games[ROOM_JID]['players'].append(user_nick) self.assertTrue(self.plugin.isPlayer(ROOM_JID, user_nick)) self.assertFalse(self.plugin.isPlayer(ROOM_JID, self.plugin_0045.getNick(0, 2))) def test_checkWaitAuth(self): def check(value, other_players, confirmed, rest): room = self.plugin_0045.getRoom(0, 0) self.assertEqual((value, confirmed, rest), self.plugin._checkWaitAuth(room, other_players)) self.reinit() self.initGame(0, 0) other_players = [Const.JID[1], Const.JID[3]] self.plugin.wait_mode = self.plugin.FOR_NONE check(True, [], [], []) check(True, [Const.JID[0]], [], [Const.JID[0]]) # getRoomNickOfUser checks for the other users only check(True, other_players, [], other_players) self.plugin.wait_mode = self.plugin.FOR_ALL check(True, [], [], []) check(False, [Const.JID[0]], [], [Const.JID[0]]) check(False, other_players, [], other_players) self.plugin_0045.joinRoom(0, 1) check(False, other_players, [], other_players) self.plugin_0045.joinRoom(0, 4) check(False, other_players, [self.plugin_0045.getNickOfUser(0, 1, 0)], [Const.JID[3]]) self.plugin_0045.joinRoom(0, 3) check(True, other_players, [self.plugin_0045.getNickOfUser(0, 1, 0), self.plugin_0045.getNickOfUser(0, 3, 0)], []) other_players = [Const.JID[1], Const.JID[3], Const.JID[2]] # the following assertion is True because Const.JID[1] and Const.JID[2] have the same userhost check(True, other_players, [self.plugin_0045.getNickOfUser(0, 1, 0), self.plugin_0045.getNickOfUser(0, 3, 0), self.plugin_0045.getNickOfUser(0, 2, 0)], []) def test_prepareRoom_trivial(self): self.reinit() other_players = [] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) self.assertTrue(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[0], Const.JID[0].user)) self.assertTrue(self.plugin._checkInviteAuth(ROOM_JID, Const.JID[0].user)) self.assertEqual((True, [], []), self.plugin._checkWaitAuth(ROOM_JID, [])) self.assertTrue(self.plugin.isReferee(ROOM_JID, Const.JID[0].user)) self.assertTrue(self.plugin.isPlayer(ROOM_JID, Const.JID[0].user)) self.assertEqual((False, True), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) def test_prepareRoom_invite(self): self.reinit() other_players = [Const.JID[1], Const.JID[2]] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) room = self.plugin_0045.getRoom(0, 0) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) self.assertTrue(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[1], Const.JID[1].user)) self.assertFalse(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[3], Const.JID[3].user)) self.assertFalse(self.plugin._checkInviteAuth(ROOM_JID, Const.JID[1].user)) self.assertEqual((True, [], other_players), self.plugin._checkWaitAuth(room, other_players)) player2_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.userJoinedTrigger(room, room.roster[player2_nick], PROFILE) self.assertTrue(self.plugin.isPlayer(ROOM_JID, player2_nick)) self.assertTrue(self.plugin._checkInviteAuth(ROOM_JID, player2_nick)) self.assertFalse(self.plugin.isReferee(ROOM_JID, player2_nick)) self.assertTrue(self.plugin.isPlayer(ROOM_JID, player2_nick)) self.assertTrue(self.plugin.isPlayer(ROOM_JID, self.plugin_0045.getNickOfUser(0, 2, 0))) self.assertFalse(self.plugin.isPlayer(ROOM_JID, "xxx")) self.assertEqual((False, False), self.plugin._checkCreateGameAndInit(ROOM_JID, Const.PROFILE[1])) def test_prepareRoom_score1(self): self.reinit(player_init={'score': 0}) other_players = [Const.JID[1], Const.JID[2]] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) room = self.plugin_0045.getRoom(0, 0) self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) self.assertTrue(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[1], Const.JID[1].user)) self.assertFalse(self.plugin._checkJoinAuth(ROOM_JID, Const.JID[3], Const.JID[3].user)) self.assertFalse(self.plugin._checkInviteAuth(ROOM_JID, Const.JID[1].user)) self.assertEqual((False, [], other_players), self.plugin._checkWaitAuth(room, other_players)) user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE) self.assertTrue(self.plugin.isPlayer(ROOM_JID, user_nick)) self.assertFalse(self.plugin._checkInviteAuth(ROOM_JID, user_nick)) self.assertFalse(self.plugin.isReferee(ROOM_JID, user_nick)) self.assertTrue(self.plugin.isPlayer(ROOM_JID, user_nick)) # the following assertion is True because Const.JID[1] and Const.JID[2] have the same userhost self.assertTrue(self.plugin.isPlayer(ROOM_JID, self.plugin_0045.getNickOfUser(0, 2, 0))) # the following assertion is True because Const.JID[1] nick in the room is equal to Const.JID[3].user self.assertTrue(self.plugin.isPlayer(ROOM_JID, Const.JID[3].user)) # but Const.JID[3] is actually not in the room self.assertEqual(self.plugin_0045.getNickOfUser(0, 3, 0), None) self.assertEqual((True, False), self.plugin._checkCreateGameAndInit(ROOM_JID, Const.PROFILE[0])) def test_prepareRoom_score2(self): self.reinit(player_init={'score': 0}) other_players = [Const.JID[1], Const.JID[4]] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) room = self.plugin_0045.getRoom(0, 0) user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE) self.assertEqual((True, False), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) user_nick = self.plugin_0045.joinRoom(0, 4) self.plugin.userJoinedTrigger(room, room.roster[user_nick], PROFILE) self.assertEqual((False, True), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) def test_userJoinedTrigger(self): self.reinit(player_init={"xxx": "xyz"}) other_players = [Const.JID[1], Const.JID[3]] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) nicks = [self.plugin_0045.getNick(0, 0)] self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "players", nicks)) self.assertTrue(len(self.plugin.invitations[ROOM_JID]) == 1) # wrong profile user_nick = self.plugin_0045.joinRoom(0, 1) room = self.plugin_0045.getRoom(0, 1) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[1]), OTHER_PROFILE) self.assertEqual(self.host.getSentMessage(0), None) # no new message has been sent self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) # game not started # referee profile, user is allowed, wait for one more room = self.plugin_0045.getRoom(0, 0) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[1]), PROFILE) nicks.append(user_nick) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "players", nicks)) self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) # game not started # referee profile, user is not allowed user_nick = self.plugin_0045.joinRoom(0, 4) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[4]), PROFILE) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(JID(ROOM_JID.userhost() + '/' + user_nick), "normal", "players", nicks)) self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) # game not started # referee profile, user is allowed, everybody here user_nick = self.plugin_0045.joinRoom(0, 3) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[3]), PROFILE) nicks.append(user_nick) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "started", nicks)) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) # game started self.assertTrue(len(self.plugin.invitations[ROOM_JID]) == 0) # wait for none self.reinit() self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) self.assertNotEqual(self.host.getSentMessage(0), None) # init messages room = self.plugin_0045.getRoom(0, 0) nicks = [self.plugin_0045.getNick(0, 0)] user_nick = self.plugin_0045.joinRoom(0, 3) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[3]), PROFILE) nicks.append(user_nick) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "started", nicks)) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) def test_userLeftTrigger(self): self.reinit(player_init={"xxx": "xyz"}) other_players = [Const.JID[1], Const.JID[3], Const.JID[4]] self.plugin.prepareRoom(other_players, ROOM_JID, PROFILE) room = self.plugin_0045.getRoom(0, 0) nicks = [self.plugin_0045.getNick(0, 0)] self.assertEqual(self.plugin.invitations[ROOM_JID][0][1], [Const.JID[1].userhostJID(), Const.JID[3].userhostJID(), Const.JID[4].userhostJID()]) # one user joins user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[1]), PROFILE) nicks.append(user_nick) # the user leaves self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) room = self.plugin_0045.getRoom(0, 1) # to not call self.plugin_0045.leaveRoom(0, 1) here, we are testing the trigger with a wrong profile self.plugin.userLeftTrigger(room, User(user_nick, Const.JID[1]), Const.PROFILE[1]) # not the referee self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) room = self.plugin_0045.getRoom(0, 0) user_nick = self.plugin_0045.leaveRoom(0, 1) self.plugin.userLeftTrigger(room, User(user_nick, Const.JID[1]), PROFILE) # referee nicks.pop() self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) # all the users join user_nick = self.plugin_0045.joinRoom(0, 1) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[1]), PROFILE) nicks.append(user_nick) user_nick = self.plugin_0045.joinRoom(0, 3) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[3]), PROFILE) nicks.append(user_nick) user_nick = self.plugin_0045.joinRoom(0, 4) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[4]), PROFILE) nicks.append(user_nick) self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) self.assertTrue(len(self.plugin.invitations[ROOM_JID]) == 0) # one user leaves user_nick = self.plugin_0045.leaveRoom(0, 4) self.plugin.userLeftTrigger(room, User(user_nick, Const.JID[4]), PROFILE) nicks.pop() self.assertEqual(self.plugin.invitations[ROOM_JID][0][1], [Const.JID[4].userhostJID()]) # another leaves user_nick = self.plugin_0045.leaveRoom(0, 3) self.plugin.userLeftTrigger(room, User(user_nick, Const.JID[3]), PROFILE) nicks.pop() self.assertEqual(self.plugin.invitations[ROOM_JID][0][1], [Const.JID[4].userhostJID(), Const.JID[3].userhostJID()]) # they can join again user_nick = self.plugin_0045.joinRoom(0, 3) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[3]), PROFILE) nicks.append(user_nick) user_nick = self.plugin_0045.joinRoom(0, 4) self.plugin.userJoinedTrigger(room, User(user_nick, Const.JID[4]), PROFILE) nicks.append(user_nick) self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) self.assertTrue(len(self.plugin.invitations[ROOM_JID]) == 0) def test__checkCreateGameAndInit(self): self.reinit() helpers.muteLogging() self.assertEqual((False, False), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) helpers.unmuteLogging() nick = self.plugin_0045.joinRoom(0, 0) self.assertEqual((True, False), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) self.assertTrue(self.plugin._gameExists(ROOM_JID, False)) self.assertFalse(self.plugin._gameExists(ROOM_JID, True)) self.assertTrue(self.plugin.isReferee(ROOM_JID, nick)) helpers.muteLogging() self.assertEqual((False, False), self.plugin._checkCreateGameAndInit(ROOM_JID, OTHER_PROFILE)) helpers.unmuteLogging() self.plugin_0045.joinRoom(0, 1) self.assertEqual((False, False), self.plugin._checkCreateGameAndInit(ROOM_JID, OTHER_PROFILE)) self.plugin.createGame(ROOM_JID, [Const.JID[1]], PROFILE) self.assertEqual((False, True), self.plugin._checkCreateGameAndInit(ROOM_JID, PROFILE)) self.assertEqual((False, False), self.plugin._checkCreateGameAndInit(ROOM_JID, OTHER_PROFILE)) def test_createGame(self): self.reinit(player_init={"xxx": "xyz"}) nicks = [] for i in [0, 1, 3, 4]: nicks.append(self.plugin_0045.joinRoom(0, i)) # game not exists self.plugin.createGame(ROOM_JID, nicks, PROFILE) self.assertTrue(self.plugin._gameExists(ROOM_JID, True)) self.assertEqual(self.plugin.games[ROOM_JID]['players'], nicks) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "started", nicks)) for nick in nicks: self.assertEqual('init', self.plugin.games[ROOM_JID]['status'][nick]) self.assertEqual(self.plugin.player_init, self.plugin.games[ROOM_JID]['players_data'][nick]) self.plugin.games[ROOM_JID]['players_data'][nick]["xxx"] = nick for nick in nicks: # checks that a copy of self.player_init has been done and not a reference self.assertEqual(nick, self.plugin.games[ROOM_JID]['players_data'][nick]['xxx']) # game exists, current profile is referee self.reinit(player_init={"xxx": "xyz"}) self.initGame(0, 0) self.plugin.games[ROOM_JID]['started'] = True self.plugin.createGame(ROOM_JID, nicks, PROFILE) self.assertEqual(self.host.getSentMessageXml(0), self._expectedMessage(ROOM_JID, "groupchat", "started", nicks)) # game exists, current profile is not referee self.reinit(player_init={"xxx": "xyz"}) self.initGame(0, 0) self.plugin.games[ROOM_JID]['started'] = True self.plugin_0045.joinRoom(0, 1) self.plugin.createGame(ROOM_JID, nicks, OTHER_PROFILE) self.assertEqual(self.host.getSentMessage(0), None) # no sync message has been sent by other_profile sat-0.6.1.1+hg20180208/src/test/test_plugin_xep_0033.py0000644000175500017600000001737713243470025022010 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . """ Plugin extended addressing stanzas """ from constants import Const from sat.test import helpers from sat.plugins import plugin_xep_0033 as plugin from sat.core.exceptions import CancelError from twisted.internet import defer from wokkel.generic import parseXml from twisted.words.protocols.jabber.jid import JID PROFILE_INDEX = 0 PROFILE = Const.PROFILE[PROFILE_INDEX] JID_STR_FROM = Const.JID_STR[1] JID_STR_TO = Const.PROFILE_DICT[PROFILE].host JID_STR_X_TO = Const.JID_STR[0] JID_STR_X_CC = Const.JID_STR[1] JID_STR_X_BCC = Const.JID_STR[2] ADDRS = ('to', JID_STR_X_TO, 'cc', JID_STR_X_CC, 'bcc', JID_STR_X_BCC) class XEP_0033Test(helpers.SatTestCase): def setUp(self): self.host = helpers.FakeSAT() self.plugin = plugin.XEP_0033(self.host) def test_messageReceived(self): self.host.memory.reinit() xml = u""" test
""" % (JID_STR_FROM, JID_STR_TO, JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC) stanza = parseXml(xml.encode("utf-8")) treatments = defer.Deferred() self.plugin.messageReceivedTrigger(self.host.getClient(PROFILE), stanza, treatments) data = {'extra': {}} def cb(data): expected = ('to', JID_STR_X_TO, 'cc', JID_STR_X_CC, 'bcc', JID_STR_X_BCC) msg = 'Expected: %s\nGot: %s' % (expected, data['extra']['addresses']) self.assertEqual(data['extra']['addresses'], '%s:%s\n%s:%s\n%s:%s\n' % expected, msg) treatments.addCallback(cb) return treatments.callback(data) def _get_mess_data(self): mess_data = {"to": JID(JID_STR_TO), "type": "chat", "message": "content", "extra": {} } mess_data["extra"]["address"] = '%s:%s\n%s:%s\n%s:%s\n' % ADDRS original_stanza = u""" content """ % (JID_STR_FROM, JID_STR_TO) mess_data['xml'] = parseXml(original_stanza.encode("utf-8")) return mess_data def _assertAddresses(self, mess_data): """The mess_data that we got here has been modified by self.plugin.messageSendTrigger, check that the addresses element has been added to the stanza.""" expected = self._get_mess_data()['xml'] addresses_extra = """
""" % ADDRS addresses_element = parseXml(addresses_extra.encode('utf-8')) expected.addChild(addresses_element) self.assertEqualXML(mess_data['xml'].toXml().encode("utf-8"), expected.toXml().encode("utf-8")) def _checkSentAndStored(self): """Check that all the recipients got their messages and that the history has been filled. /!\ see the comments in XEP_0033.sendAndStoreMessage""" sent = [] stored = [] d_list = [] def cb(entities, to_jid): if host in entities: if host not in sent: # send the message to the entity offering the feature sent.append(host) stored.append(host) stored.append(to_jid) # store in history for each recipient else: # feature not supported, use normal behavior sent.append(to_jid) stored.append(to_jid) helpers.unmuteLogging() for to_s in (JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC): to_jid = JID(to_s) host = JID(to_jid.host) helpers.muteLogging() d = self.host.findFeaturesSet([plugin.NS_ADDRESS], jid_=host, profile=PROFILE) d.addCallback(cb, to_jid) d_list.append(d) def cb_list(dummy): msg = "/!\ see the comments in XEP_0033.sendAndStoreMessage" sent_recipients = [JID(elt['to']) for elt in self.host.getSentMessages(PROFILE_INDEX)] self.assertEqualUnsortedList(sent_recipients, sent, msg) self.assertEqualUnsortedList(self.host.stored_messages, stored, msg) return defer.DeferredList(d_list).addCallback(cb_list) def _trigger(self, data): """Execute self.plugin.messageSendTrigger with a different logging level to not pollute the output, then check that the plugin did its job. It should abort sending the message or add the extended addressing information to the stanza. @param data: the data to be processed by self.plugin.messageSendTrigger """ pre_treatments = defer.Deferred() post_treatments = defer.Deferred() helpers.muteLogging() self.plugin.messageSendTrigger(self.host.getClient[PROFILE], data, pre_treatments, post_treatments) post_treatments.callback(data) helpers.unmuteLogging() post_treatments.addCallbacks(self._assertAddresses, lambda failure: failure.trap(CancelError)) return post_treatments def test_messageSendTriggerFeatureNotSupported(self): # feature is not supported, abort the message self.host.memory.reinit() data = self._get_mess_data() return self._trigger(data) def test_messageSendTriggerFeatureSupported(self): # feature is supported by the main target server self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) data = self._get_mess_data() d = self._trigger(data) return d.addCallback(lambda dummy: self._checkSentAndStored()) def test_messageSendTriggerFeatureFullySupported(self): # feature is supported by all target servers self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) for dest in (JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC): self.host.addFeature(JID(JID(dest).host), plugin.NS_ADDRESS, PROFILE) data = self._get_mess_data() d = self._trigger(data) return d.addCallback(lambda dummy: self._checkSentAndStored()) def test_messageSendTriggerFixWrongEntity(self): # check that a wrong recipient entity is fixed by the backend self.host.reinit() self.host.addFeature(JID(JID_STR_TO), plugin.NS_ADDRESS, PROFILE) for dest in (JID_STR_X_TO, JID_STR_X_CC, JID_STR_X_BCC): self.host.addFeature(JID(JID(dest).host), plugin.NS_ADDRESS, PROFILE) data = self._get_mess_data() data["to"] = JID(JID_STR_X_TO) d = self._trigger(data) return d.addCallback(lambda dummy: self._checkSentAndStored()) sat-0.6.1.1+hg20180208/src/test/helpers.py0000644000175500017600000004275413243470025017571 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 . ## logging configuration for tests ## from sat.core import log_config log_config.satConfigure() import logging from sat.core.log import getLogger getLogger().setLevel(logging.WARNING) # put this to DEBUG when needed from sat.core import exceptions from sat.tools import config as tools_config from constants import Const as C from wokkel.xmppim import RosterItem from wokkel.generic import parseXml from sat.core.xmpp import SatRosterProtocol from sat.memory.memory import Params, Memory from twisted.trial.unittest import FailTest from twisted.trial import unittest from twisted.internet import defer from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish from xml.etree import cElementTree as etree from collections import Counter import re def b2s(value): """Convert a bool to a unicode string used in bridge @param value: boolean value @return: unicode conversion, according to bridge convention """ return u"True" if value else u"False" def muteLogging(): """Temporarily set the logging level to CRITICAL to not pollute the output with expected errors.""" logger = getLogger() logger.original_level = logger.getEffectiveLevel() logger.setLevel(logging.CRITICAL) def unmuteLogging(): """Restore the logging level after it has been temporarily disabled.""" logger = getLogger() logger.setLevel(logger.original_level) class DifferentArgsException(FailTest): pass class DifferentXMLException(FailTest): pass class DifferentListException(FailTest): pass class FakeSAT(object): """Class to simulate a SAT instance""" def __init__(self): self.bridge = FakeBridge() self.memory = FakeMemory(self) self.trigger = FakeTriggerManager() self.profiles = {} self.reinit() def reinit(self): """This can be called by tests that check for sent and stored messages, uses FakeClient or get/set some other data that need to be cleaned""" for profile in self.profiles: self.profiles[profile].reinit() self.memory.reinit() self.stored_messages = [] self.plugins = {} self.profiles = {} def delContact(self, to, profile_key): #TODO pass def registerCallback(self, callback, *args, **kwargs): pass def messageSend(self, to_s, msg, subject=None, mess_type='auto', extra={}, profile_key='@NONE@'): self.sendAndStoreMessage({"to": JID(to_s)}) def _sendMessageToStream(self, mess_data, client): """Save the information to check later to whom messages have been sent. @param mess_data: message data dictionnary @param client: profile's client """ client.xmlstream.send(mess_data['xml']) return mess_data def _storeMessage(self, mess_data, client): """Save the information to check later if entries have been added to the history. @param mess_data: message data dictionnary @param client: profile's client """ self.stored_messages.append(mess_data["to"]) return mess_data def sendMessageToBridge(self, mess_data, client): """Simulate the message being sent to the frontends. @param mess_data: message data dictionnary @param client: profile's client """ return mess_data # TODO def getProfileName(self, profile_key): """Get the profile name from the profile_key""" return profile_key def getClient(self, profile_key): """Convenient method to get client from profile key @return: client or None if it doesn't exist""" profile = self.memory.getProfileName(profile_key) if not profile: raise exceptions.ProfileKeyUnknown if profile not in self.profiles: self.profiles[profile] = FakeClient(self, profile) return self.profiles[profile] def getJidNStream(self, profile_key): """Convenient method to get jid and stream from profile key @return: tuple (jid, xmlstream) from profile, can be None""" return (C.PROFILE_DICT[profile_key], None) def isConnected(self, profile): return True def getSentMessages(self, profile_index): """Return all the sent messages (in the order they have been sent) and empty the list. Called by tests. FakeClient instances associated to each profile must have been previously initialized with the method FakeSAT.getClient. @param profile_index: index of the profile to consider (cf. C.PROFILE) @return: the sent messages for given profile, or None""" try: tmp = self.profiles[C.PROFILE[profile_index]].xmlstream.sent self.profiles[C.PROFILE[profile_index]].xmlstream.sent = [] return tmp except IndexError: return None def getSentMessage(self, profile_index): """Pop and return the sent message in first position (works like a FIFO). Called by tests. FakeClient instances associated to each profile must have been previously initialized with the method FakeSAT.getClient. @param profile_index: index of the profile to consider (cf. C.PROFILE) @return: the sent message for given profile, or None""" try: return self.profiles[C.PROFILE[profile_index]].xmlstream.sent.pop(0) except IndexError: return None def getSentMessageXml(self, profile_index): """Pop and return the sent message in first position (works like a FIFO). Called by tests. FakeClient instances associated to each profile must have been previously initialized with the method FakeSAT.getClient. @return: XML representation of the sent message for given profile, or None""" entry = self.getSentMessage(profile_index) return entry.toXml() if entry else None def findFeaturesSet(self, features, identity=None, jid_=None, profile=C.PROF_KEY_NONE): """Call self.addFeature from your tests to change the return value. @return: a set of entities """ client = self.getClient(profile) if jid_ is None: jid_ = JID(client.jid.host) try: if set(features).issubset(client.features[jid_]): return defer.succeed(set([jid_])) except (TypeError, AttributeError, KeyError): pass return defer.succeed(set()) def addFeature(self, jid_, feature, profile_key): """Add a feature to an entity. To be called from your tests when needed. """ client = self.getClient(profile_key) if not hasattr(client, 'features'): client.features = {} if jid_ not in client.features: client.features[jid_] = set() client.features[jid_].add(feature) class FakeBridge(object): """Class to simulate and test bridge calls""" def __init__(self): self.expected_calls = {} def expectCall(self, name, *check_args, **check_kwargs): if hasattr(self, name): # queue this new call as one already exists self.expected_calls.setdefault(name, []) self.expected_calls[name].append((check_args, check_kwargs)) return def checkCall(*args, **kwargs): if args != check_args or kwargs != check_kwargs: print "\n\n--------------------" print "Args are not equals:" print "args\n----\n%s (sent)\n%s (wanted)" % (args, check_args) print "kwargs\n------\n%s (sent)\n%s (wanted)" % (kwargs, check_kwargs) print "--------------------\n\n" raise DifferentArgsException delattr(self, name) if name in self.expected_calls: # register the next call args, kwargs = self.expected_calls[name].pop(0) if len(self.expected_calls[name]) == 0: del self.expected_calls[name] self.expectCall(name, *args, **kwargs) setattr(self, name, checkCall) def addMethod(self, name, int_suffix, in_sign, out_sign, method, async=False, doc=None): pass def addSignal(self, name, int_suffix, signature): pass def addTestCallback(self, name, method): """This can be used to register callbacks for bridge methods AND signals. Contrary to expectCall, this will not check if the method or signal is called/sent with the correct arguments, it will instead run the callback of your choice.""" setattr(self, name, method) class FakeParams(Params): """Class to simulate and test params object. The methods of Params that could not be run (for example those using the storage attribute must be overwritten by a naive simulation of what they should do.""" def __init__(self, host, storage): Params.__init__(self, host, storage) self.params = {} # naive simulation of values storage def setParam(self, name, value, category, security_limit=-1, profile_key='@NONE@'): profile = self.getProfileName(profile_key) self.params.setdefault(profile, {}) self.params[profile_key][(category, name)] = value def getParamA(self, name, category, attr="value", profile_key='@NONE@'): profile = self.getProfileName(profile_key) return self.params[profile][(category, name)] def getProfileName(self, profile_key, return_profile_keys=False): if profile_key == '@DEFAULT@': return C.PROFILE[0] elif profile_key == '@NONE@': raise exceptions.ProfileNotSetError else: return profile_key def loadIndParams(self, profile, cache=None): self.params[profile] = {} return defer.succeed(None) class FakeMemory(Memory): """Class to simulate and test memory object""" def __init__(self, host): # do not call Memory.__init__, we just want to call the methods that are # manipulating basic stuff, the others should be overwritten when needed self.host = host self.params = FakeParams(host, None) self.config = tools_config.parseMainConf() self.reinit() def reinit(self): """Tests that manipulate params, entities, features should re-initialise the memory first to not fake the result.""" self.params.load_default_params() self.params.params.clear() self.params.frontends_cache = [] self.entities_data = {} def getProfileName(self, profile_key, return_profile_keys=False): return self.params.getProfileName(profile_key, return_profile_keys) def addToHistory(self, from_jid, to_jid, message, _type='chat', extra=None, timestamp=None, profile="@NONE@"): pass def addContact(self, contact_jid, attributes, groups, profile_key='@DEFAULT@'): pass def setPresenceStatus(self, contact_jid, show, priority, statuses, profile_key='@DEFAULT@'): pass def addWaitingSub(self, type_, contact_jid, profile_key): pass def delWaitingSub(self, contact_jid, profile_key): pass def updateEntityData(self, entity_jid, key, value, silent=False, profile_key="@NONE@"): self.entities_data.setdefault(entity_jid, {}) self.entities_data[entity_jid][key] = value def getEntityData(self, entity_jid, keys, profile_key): result = {} for key in keys: result[key] = self.entities_data[entity_jid][key] return result class FakeTriggerManager(object): def add(self, point_name, callback, priority=0): pass def point(self, point_name, *args, **kwargs): """We always return true to continue the action""" return True class FakeRosterProtocol(SatRosterProtocol): """This class is used by FakeClient (one instance per profile)""" def __init__(self, host, parent): SatRosterProtocol.__init__(self, host) self.parent = parent self._jids = {} self.addItem(parent.jid.userhostJID()) def addItem(self, jid, *args, **kwargs): if not args and not kwargs: # defaults values setted for the tests only kwargs["subscriptionTo"] = True kwargs["subscriptionFrom"] = True roster_item = RosterItem(jid, *args, **kwargs) attrs = {'to': b2s(roster_item.subscriptionTo), 'from': b2s(roster_item.subscriptionFrom), 'ask': b2s(roster_item.pendingOut)} if roster_item.name: attrs['name'] = roster_item.name self.host.bridge.expectCall("newContact", jid.full(), attrs, roster_item.groups, self.parent.profile) self._jids[jid] = roster_item self._registerItem(roster_item) class FakeXmlStream(object): """This class is used by FakeClient (one instance per profile)""" def __init__(self): self.sent = [] def send(self, obj): """Save the sent messages to compare them later. @param obj (domish.Element, str or unicode): message to send """ if not isinstance(obj, domish.Element): assert(isinstance(obj, str) or isinstance(obj, unicode)) obj = parseXml(obj) if obj.name == 'iq': # IQ request expects an answer, return the request itself so # you can check if it has been well built by your plugin. self.iqDeferreds[obj['id']].callback(obj) self.sent.append(obj) return defer.succeed(None) def addObserver(self, *argv): pass class FakeClient(object): """Tests involving more than one profile need one instance of this class per profile""" def __init__(self, host, profile=None): self.host = host self.profile = profile if profile else C.PROFILE[0] self.jid = C.PROFILE_DICT[self.profile] self.roster = FakeRosterProtocol(host, self) self.xmlstream = FakeXmlStream() def reinit(self): self.xmlstream = FakeXmlStream() def send(self, obj): return self.xmlstream.send(obj) class SatTestCase(unittest.TestCase): def assertEqualXML(self, xml, expected, ignore_blank=False): def equalElt(got_elt, exp_elt): if ignore_blank: for elt in got_elt, exp_elt: for attr in ('text', 'tail'): value = getattr(elt, attr) try: value = value.strip() or None except AttributeError: value = None setattr(elt, attr, value) if (got_elt.tag != exp_elt.tag): print "XML are not equals (elt %s/%s):" % (got_elt, exp_elt) print "tag: got [%s] expected: [%s]" % (got_elt.tag, exp_elt.tag) return False if (got_elt.attrib != exp_elt.attrib): print "XML are not equals (elt %s/%s):" % (got_elt, exp_elt) print "attribs: got %s expected %s" % (got_elt.attrib, exp_elt.attrib) return False if (got_elt.tail != exp_elt.tail or got_elt.text != exp_elt.text): print "XML are not equals (elt %s/%s):" % (got_elt, exp_elt) print "text: got [%s] expected: [%s]" % (got_elt.text, exp_elt.text) print "tail: got [%s] expected: [%s]" % (got_elt.tail, exp_elt.tail) return False if (len(got_elt) != len(exp_elt)): print "XML are not equals (elt %s/%s):" % (got_elt, exp_elt) print "children len: got %d expected: %d" % (len(got_elt), len(exp_elt)) return False for idx, child in enumerate(got_elt): if not equalElt(child, exp_elt[idx]): return False return True def remove_blank(xml): lines = [line.strip() for line in re.sub(r'[ \t\r\f\v]+', ' ', xml).split('\n')] return '\n'.join([line for line in lines if line]) xml_elt = etree.fromstring(remove_blank(xml) if ignore_blank else xml) expected_elt = etree.fromstring(remove_blank(expected) if ignore_blank else expected) if not equalElt(xml_elt, expected_elt): print "---" print "XML are not equals:" print "got:\n-\n%s\n-\n\n" % etree.tostring(xml_elt, encoding='utf-8') print "was expecting:\n-\n%s\n-\n\n" % etree.tostring(expected_elt, encoding='utf-8') print "---" raise DifferentXMLException def assertEqualUnsortedList(self, a, b, msg): counter_a = Counter(a) counter_b = Counter(b) if counter_a != counter_b: print "---" print "Unsorted lists are not equals:" print "got : %s" % counter_a print "was expecting: %s" % counter_b if msg: print msg print "---" raise DifferentListException sat-0.6.1.1+hg20180208/src/sat.sh0000755000175500017600000000544213243470025015715 0ustar debaclelocal_src#!/bin/sh DEBUG="" DAEMON="" PYTHON="python2" TWISTD="$(which twistd)" kill_process() { # $1 is the file containing the PID to kill, $2 is the process name if [ -f $1 ]; then PID=`cat $1` if ps -p $PID > /dev/null; then printf "Terminating $2... " kill $PID while ps -p $PID > /dev/null; do sleep 0.2 done printf "OK\n" else echo "No running process of ID $PID... removing PID file" rm -f $1 fi else echo "$2 is probably not running (PID file doesn't exist)" fi } #We use python to parse config files eval `"$PYTHON" << PYTHONEND from sat.core.constants import Const as C from sat.memory.memory import fixLocalDir from ConfigParser import SafeConfigParser from os.path import expanduser, join import sys import codecs import locale sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) fixLocalDir() # XXX: tmp update code, will be removed in the future config = SafeConfigParser(defaults=C.DEFAULT_CONFIG) try: config.read(C.CONFIG_FILES) except: print ("echo \"/!\\ Can't read main config ! Please check the syntax\";") print ("exit 1") sys.exit() env=[] env.append("PID_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'pid_dir')),'')) env.append("LOG_DIR='%s'" % join(expanduser(config.get('DEFAULT', 'log_dir')),'')) env.append("APP_NAME='%s'" % C.APP_NAME) env.append("APP_NAME_FILE='%s'" % C.APP_NAME_FILE) print ";".join(env) PYTHONEND ` APP_NAME="$APP_NAME" PID_FILE="$PID_DIR$APP_NAME_FILE.pid" LOG_FILE="$LOG_DIR$APP_NAME_FILE.log" RUNNING_MSG="$APP_NAME is running" NOT_RUNNING_MSG="$APP_NAME is *NOT* running" # if there is one argument which is "stop", then we kill SàT if [ $# -eq 1 ];then if [ $1 = "stop" ];then kill_process $PID_FILE "$APP_NAME" exit 0 elif [ $1 = "debug" ];then echo "Launching $APP_NAME in debug mode" DEBUG="--debug" elif [ $1 = "fg" ];then echo "Launching $APP_NAME in foreground mode" DAEMON="n" elif [ $1 = "status" ];then if [ -f $PID_FILE ]; then PID=`cat $PID_FILE` ps -p$PID 2>&1 > /dev/null if [ $? = 0 ];then echo "$RUNNING_MSG (pid: $PID)" exit 0 else echo "$NOT_RUNNING_MSG, but a pid file is present (bad exit ?): $PID_FILE" exit 2 fi else echo "$NOT_RUNNING_MSG" exit 1 fi else echo "bad argument, please use one of (stop, debug, fg, status) or no argument" exit 1 fi fi MAIN_OPTIONS="-${DAEMON}o" #Don't change the next lines AUTO_OPTIONS="" ADDITIONAL_OPTIONS="--pidfile $PID_FILE --logfile $LOG_FILE $AUTO_OPTIONS $DEBUG" log_dir=`dirname "$LOG_FILE"` if [ ! -d $log_dir ] ; then mkdir $log_dir fi exec $PYTHON $TWISTD $MAIN_OPTIONS $ADDITIONAL_OPTIONS $APP_NAME_FILE sat-0.6.1.1+hg20180208/src/core/0002755000175500017600000000000013243470024015513 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/core/xmpp.py0000644000175500017600000013700313243470024017053 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.memory import cache from twisted.internet import task, defer from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.words.protocols.jabber import xmlstream from twisted.words.protocols.jabber import error from twisted.words.protocols.jabber import jid from twisted.words.xish import domish from twisted.python import failure from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel from wokkel import component from wokkel import delay from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from zope.interface import implements import time import calendar import uuid import sys class SatXMPPEntity(object): """Common code for Client and Component""" def __init__(self, host_app, profile, max_retries): self.factory.clientConnectionLost = self.connectionLost self.factory.maxRetries = max_retries # when self._connected is None, we are not connected # else, it's a deferred which fire on disconnection self._connected = None self.profile = profile self.host_app = host_app self.cache = cache.Cache(host_app, profile) self._mess_id_uid = {} # map from message id to uid used in history. Key: (full_jid,message_id) Value: uid self.conn_deferred = defer.Deferred() ## initialisation ## @defer.inlineCallbacks def _callConnectionTriggers(self): """Call conneting trigger prepare connected trigger @param plugins(iterable): plugins to use @return (list[object, callable]): plugin to trigger tuples with: - plugin instance - profileConnected* triggers (to call after connection) """ plugin_conn_cb = [] for plugin in self._getPluginsList(): # we check if plugin handle client mode if plugin.is_handler: plugin.getHandler(self).setHandlerParent(self) # profileConnecting/profileConnected methods handling # profile connecting is called right now (before actually starting client) connecting_cb = getattr(plugin, "profileConnecting" + self.trigger_suffix, None) if connecting_cb is not None: yield connecting_cb(self) # profile connected is called after client is ready and roster is got connected_cb = getattr(plugin, "profileConnected" + self.trigger_suffix, None) if connected_cb is not None: plugin_conn_cb.append((plugin, connected_cb)) defer.returnValue(plugin_conn_cb) def _getPluginsList(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 _createSubProtocols(self): return def entityConnected(self): """Called once connection is done may return a Deferred, to perform initialisation tasks """ return @classmethod @defer.inlineCallbacks def startConnection(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 scrash # 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 # try: port = int(host.memory.getParamA(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 = yield host.memory.asyncGetParamA("Password", "Connection", profile_key=profile) entity = host.profiles[profile] = cls(host, profile, jid.JID(host.memory.getParamA("JabberID", "Connection", profile_key=profile)), password, host.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile) or None, port, max_retries) entity._createSubProtocols() entity.fallBack = SatFallbackHandler(host) entity.fallBack.setHandlerParent(entity) entity.versionHandler = SatVersionHandler(C.APP_NAME_FULL, host.full_version) entity.versionHandler.setHandlerParent(entity) entity.identityHandler = SatIdentityHandler() entity.identityHandler.setHandlerParent(entity) log.debug(_("setting plugins parents")) plugin_conn_cb = yield entity._callConnectionTriggers() entity.startService() yield entity.getConnectionDeferred() yield defer.maybeDeferred(entity.entityConnected) # Call profileConnected callback for all plugins, and print error message if any of them fails conn_cb_list = [] for dummy, callback in plugin_conn_cb: conn_cb_list.append(defer.maybeDeferred(callback, entity)) list_d = defer.DeferredList(conn_cb_list) def logPluginResults(results): all_succeed = all([success for success, result in results]) if not all_succeed: log.error(_(u"Plugins initialisation error")) for idx, (success, result) in enumerate(results): if not success: log.error(u"error (plugin %(name)s): %(failure)s" % {'name': plugin_conn_cb[idx][0]._info['import_name'], 'failure': result}) yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze # TODO: mesure launch time of each plugin def getConnectionDeferred(self): """Return a deferred which fire when the client is connected""" return self.conn_deferred def _disconnectionCb(self, dummy): self._connected = None def _disconnectionEb(self, failure_): log.error(_(u"Error while disconnecting: {}".format(failure_))) def _authd(self, xmlstream): if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile): return super(SatXMPPEntity, self)._authd(xmlstream) # 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 = defer.Deferred() self._connected.addCallback(self._cleanConnection) self._connected.addCallback(self._disconnectionCb) self._connected.addErrback(self._disconnectionEb) log.info(_("********** [%s] CONNECTED **********") % self.profile) self.streamInitialized() self.host_app.bridge.connected(self.profile, unicode(self.jid)) # we send the signal to the clients def _finish_connection(self, dummy): self.conn_deferred.callback(None) def streamInitialized(self): """Called after _authd""" log.debug(_(u"XML stream is initialized")) self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") # Needed to avoid disconnection (specially with openfire) self.keep_alife.start(C.XMPP_KEEP_ALIFE) 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(_(u"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 connectionLost(self, connector, reason): try: self.keep_alife.stop() except AttributeError: log.debug(_("No keep_alife")) if self._connected is not None: self.host_app.bridge.disconnected(self.profile) # we send the signal to the clients self._connected.callback(None) self.host_app.purgeEntity(self.profile) # and we remove references to this client log.info(_("********** [%s] DISCONNECTED **********") % self.profile) if not self.conn_deferred.called: # FIXME: real error is not gotten here (e.g. if jid is not know by Prosody, # we should have the real error) self.conn_deferred.errback(error.StreamError(u"Server unexpectedly closed the connection")) @defer.inlineCallbacks def _cleanConnection(self, dummy): """method called on disconnection used to call profileDisconnected* triggers """ trigger_name = "profileDisconnected" + self.trigger_suffix for plugin in self._getPluginsList(): disconnected_cb = getattr(plugin, trigger_name, None) if disconnected_cb is not None: yield disconnected_cb(self) def isConnected(self): return self._connected is not None def entityDisconnect(self): log.info(_(u"Disconnecting...")) self.stopService() if self._connected is not None: return self._connected else: return defer.succeed(None) ## sending ## def IQ(self, type_=u'set', timeout=None): """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 and error stanza is returned """ iq_elt = xmlstream.IQ(self.xmlstream, type_) iq_elt.timeout = timeout return iq_elt def sendError(self, iq_elt, condition): """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).toResponse(iq_elt) self.xmlstream.send(iq_error_elt) def generateMessageXML(self, data): """Generate stanza from message data @param data(dict): 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 @return (dict) 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"].iteritems(): subject_elt = message_elt.addElement("subject", content=subject) if lang: subject_elt[(C.NS_XML, 'lang')] = lang for lang, message in data["message"].iteritems(): 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(u"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 return data def addPostXmlCallbacks(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 def sendMessage(self, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False): """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 """ 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 unicode(uuid.uuid4()), "message": message, "subject": subject, "type": mess_type, "extra": extra, "timestamp": time.time(), } pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this 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: # if to JID has a resource, the type is not 'groupchat' # we may have a groupchat message, we check if the we know this jid try: entity_type = self.host_app.memory.getEntityData(data["to"], ['type'], self.profile)["type"] #FIXME: should entity_type manage resources ? except (exceptions.UnknownEntityError, KeyError): entity_type = "contact" if entity_type == "chatroom": data["type"] = C.MESS_TYPE_GROUPCHAT else: data["type"] = C.MESS_TYPE_CHAT else: data["type"] == C.MESS_TYPE_CHAT data["type"] == C.MESS_TYPE_CHAT if data["subject"] else C.MESS_TYPE_NORMAL # 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: 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(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full())) pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data)) pre_xml_treatments.chainDeferred(post_xml_treatments) post_xml_treatments.addCallback(self.sendMessageData) if send_only: log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter")) else: self.addPostXmlCallbacks(post_xml_treatments) post_xml_treatments.addErrback(self._cancelErrorTrap) post_xml_treatments.addErrback(self.host_app.logErrback) pre_xml_treatments.callback(data) return pre_xml_treatments def _cancelErrorTrap(self, failure): """A message sending can be cancelled by a plugin treatment""" failure.trap(exceptions.CancelError) def messageAddToHistory(self, data): """Store message into database (for local history) @param data: message data dictionnary @param client: profile's client """ if data[u"type"] != C.MESS_TYPE_GROUPCHAT: # we don't add groupchat message to history, as we get them back # and they will be added then if data[u'message'] or data[u'subject']: # we need a message to store self.host_app.memory.addToHistory(self, data) else: log.warning(u"No message found") # empty body should be managed by plugins before this point return data def messageSendToBridge(self, data): """Send message to bridge, so frontends can display it @param data: message data dictionnary @param client: profile's client """ if data[u"type"] != C.MESS_TYPE_GROUPCHAT: # we don't send groupchat message to bridge, as we get them back # and they will be added the if data[u'message'] or data[u'subject']: # we need a message to send something # We send back the message, so all frontends are aware of it self.host_app.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=self.profile) else: log.warning(_(u"No message found")) return data class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient): implements(iwokkel.IDisco) trigger_suffix = "" 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(u"client", u"pc", C.APP_NAME)] if sys.platform == "android": # FIXME: temporary hack as SRV is not working on android # TODO: remove this hack and fix SRV log.info(u"FIXME: Android hack, ignoring SRV") host = user_jid.host hosts_map = host_app.memory.getConfig(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, basestring): host = host_data elif isinstance(host_data, dict): if u'host' in host_data: host = host_data[u'host'] if u'port' in host_data: port = host_data[u'port'] else: log.warning(_(u"invalid data used for host: {data}").format(data=host_data)) host_data = None if host_data is not None: log.info(u"using {host}:{port} for host {host_ori} as requested in config".format( host_ori = user_jid.host, host = host, port = port)) wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT) SatXMPPEntity.__init__(self, host_app, profile, max_retries) 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) def _getPluginsList(self): for p in self.host_app.plugins.itervalues(): if C.PLUG_MODE_CLIENT in p._info[u'modes']: yield p def _createSubProtocols(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) def entityConnected(self): # we want to be sure that we got the roster return self.roster.got_roster def addPostXmlCallbacks(self, post_xml_treatments): post_xml_treatments.addCallback(self.messageAddToHistory) post_xml_treatments.addCallback(self.messageSendToBridge) def 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 # is 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 # FIXME: trigger not used yet, can be uncommented when e2e full stanza encryption is implemented # if not self.host_app.trigger.point("send", self, obj): #  return super(SatXMPPClient, self).send(obj) def sendMessageData(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 @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 self.host_app.trigger.point("sendMessageData", self, mess_data) self.send(mess_data[u'xml']) return mess_data def feedback(self, to_jid, message): """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(jid.JID): destinee jid @param message(unicode): message to send to frontends """ self.host_app.bridge.messageNew(uid=unicode(uuid.uuid4()), timestamp=time.time(), from_jid=self.jid.full(), to_jid=to_jid.full(), message={u'': message}, subject={}, mess_type=C.MESS_TYPE_INFO, extra={}, profile=self.profile) def _finish_connection(self, dummy): self.roster.requestRoster() self.presence.available() super(SatXMPPClient, self)._finish_connection(dummy) 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 """ implements(iwokkel.IDisco) trigger_suffix = "Component" # used for to distinguish some trigger points set in SatXMPPEntity component = True sendHistory = False # XXX: set to True from entry plugin to keep messages in history for received messages 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.getEntryPoint(profile) try: self.entry_plugin = host_app.plugins[entry_point] except KeyError: raise exceptions.NotFound(_(u"The requested entry point ({entry_point}) is not available").format( entry_point = entry_point)) self.identities = [disco.DiscoIdentity(u"component", u"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(u'.', 1)[1] except IndexError: raise ValueError(u"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) def _buildDependencies(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[u'modes']: if not required: return else: log.error(_(u"Plugin {current_name} if needed for {entry_name}, but it doesn't handle component mode").format( current_name = current._info[u'import_name'], entry_name = self.entry_plugin._info[u'import_name'] )) raise exceptions.InternalError(_(u"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._checkDependencies(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._buildDependencies(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 _getPluginsList(self): # XXX: for component we don't launch all plugins triggers # but only the ones from which there is a dependency plugins = [] self._buildDependencies(self.entry_plugin, plugins) return plugins def entityConnected(self): # we can now launch entry point return self.entry_plugin.componentStart(self) def addPostXmlCallbacks(self, post_xml_treatments): if self.sendHistory: post_xml_treatments.addCallback(self.messageAddToHistory) class SatMessageProtocol(xmppim.MessageProtocol): def __init__(self, host): xmppim.MessageProtocol.__init__(self) self.host = host @staticmethod def parseMessage(message_elt, client=None): """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 """ message = {} subject = {} extra = {} data = {"from": jid.JID(message_elt['from']), "to": jid.JID(message_elt['to']), "uid": message_elt.getAttribute('uid', unicode(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} if client is not None: try: data['stanza_id'] = message_elt['id'] except KeyError: pass else: client._mess_id_uid[(data['from'], data['stanza_id'])] = data['uid'] # message for e in message_elt.elements(C.NS_CLIENT, 'body'): message[e.getAttribute((C.NS_XML,'lang'),'')] = unicode(e) # subject for e in message_elt.elements(C.NS_CLIENT, 'subject'): subject[e.getAttribute((C.NS_XML, 'lang'),'')] = unicode(e) # delay and timestamp try: delay_elt = message_elt.elements(delay.NS_DELAY, 'delay').next() except StopIteration: data['timestamp'] = time.time() else: parsed_delay = delay.Delay.fromElement(delay_elt) data['timestamp'] = calendar.timegm(parsed_delay.stamp.utctimetuple()) data['received_timestamp'] = unicode(time.time()) if parsed_delay.sender: data['delay_sender'] = parsed_delay.sender.full() return data def onMessage(self, message_elt): # TODO: handle threads client = self.parent if not 'from' in message_elt.attributes: message_elt['from'] = client.jid.host log.debug(_(u"got message from: {from_}").format(from_=message_elt['from'])) post_treat = defer.Deferred() # XXX: plugin can add their treatments to this deferred if not self.host.trigger.point("MessageReceived", client, message_elt, post_treat): return data = self.parseMessage(message_elt, client) post_treat.addCallback(self.skipEmptyMessage) post_treat.addCallback(self.addToHistory, client) post_treat.addCallback(self.bridgeSignal, client, data) post_treat.addErrback(self.cancelErrorTrap) post_treat.callback(data) def skipEmptyMessage(self, data): if not data['message'] and not data['extra'] and not data['subject']: raise failure.Failure(exceptions.CancelError("Cancelled empty message")) return data def addToHistory(self, data, client): if data.pop(u'history', None) == C.HISTORY_SKIP: log.info(u'history is skipped as requested') data[u'extra'][u'history'] = C.HISTORY_SKIP else: return self.host.memory.addToHistory(client, data) def bridgeSignal(self, dummy, client, data): try: data['extra']['received_timestamp'] = data['received_timestamp'] data['extra']['delay_sender'] = data['delay_sender'] except KeyError: pass if data is not None: self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile) return data def cancelErrorTrap(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._groups = {} # map from groups to jids: key=group value=set of jids self._jids = None # map from jids to RosterItem: key=jid value=RosterItem def rosterCb(self, roster): assert roster is not None # FIXME: must be managed with roster versioning self._groups.clear() self._jids = roster for item in roster.itervalues(): 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(u"Removing contact {} from roster because there is no presence subscription".format(item.jid)) self.removeItem(item.entity) # FIXME: to be checked else: self._registerItem(item) def _registerItem(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(u"registering item: {}".format(item.entity.full())) if item.entity.resource: log.warning(u"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(_(u"There's no subscription between you and [{}]!").format(item.entity.full())) else: log.info(_(u"You are not subscribed to [{}]!").format(item.entity.full())) if not item.subscriptionFrom: log.info(_(u"[{}] is not subscribed to you!").format(item.entity.full())) for group in item.groups: self._groups.setdefault(group, set()).add(item.entity) def requestRoster(self): """ ask the server for Roster list """ log.debug("requestRoster") d = self.getRoster().addCallback(self.rosterCb) d.chainDeferred(self.got_roster) 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 getAttributes(self, item): """Return dictionary of attributes as used in bridge from a RosterItem @param item: RosterItem @return: dictionary of attributes """ item_attr = {'to': unicode(item.subscriptionTo), 'from': unicode(item.subscriptionFrom), 'ask': unicode(item.ask) } if item.name: item_attr['name'] = item.name return item_attr def setReceived(self, request): #TODO: implement roster versioning (cf RFC 6121 §2.6) item = request.item try: # update the cache for the groups the contact has been removed from left_groups = set(self._jids[item.entity].groups).difference(item.groups) for group in left_groups: jids_set = self._groups[group] jids_set.remove(item.entity) if not jids_set: del self._groups[group] except KeyError: pass # no previous item registration (or it's been cleared) self._jids[item.entity] = item self._registerItem(item) self.host.bridge.newContact(item.entity.full(), self.getAttributes(item), item.groups, self.parent.profile) def removeReceived(self, request): entity = request.item.entity log.info(u"removing %s from roster list" % entity.full()) # we first remove item from local cache (self._groups and self._jids) try: item = self._jids.pop(entity) except KeyError: log.error(u"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(u"there is no cache for the group [%(group)s] of the removed roster item [%(jid)s]" % {"group": group, "jid": entity}) # then we send the bridge signal self.host.bridge.contactDeleted(entity.full(), self.parent.profile) def getGroups(self): """Return a list of groups""" return self._groups.keys() def getItem(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 getJids(self): """Return all jids of the roster""" return self._jids.keys() def isJidInRoster(self, entity_jid): """Return True if jid is in roster""" return entity_jid in self._jids def isPresenceAuthorised(self, entity_jid): """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 getItems(self): """Return all items of the roster""" return self._jids.values() def getJidsFromGroup(self, group): try: return self._groups[group] except KeyError: raise exceptions.UnknownGroupError(group) def getJidsSet(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.getJids()) elif type_ == C.GROUP: jids = set() for group in groups: jids.update(self.getJidsFromGroup(group)) return jids else: raise ValueError(u'Unexpected type_ {}'.format(type_)) def getNick(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.getItem(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 def send(self, obj): if not self.host.trigger.point("Presence send", self.parent, obj): return super(SatPresenceProtocol, self).send(obj) def availableReceived(self, entity, show=None, statuses=None, priority=0): log.debug(_(u"presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity': entity, C.PRESENCE_SHOW: show, C.PRESENCE_STATUSES: statuses, C.PRESENCE_PRIORITY: priority}) 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("presenceReceived", entity, show, priority, statuses, self.parent.profile): return self.host.memory.setPresenceStatus(entity, show or "", int(priority), statuses, self.parent.profile) # now it's time to notify frontends self.host.bridge.presenceUpdate(entity.full(), show or "", int(priority), statuses, self.parent.profile) def unavailableReceived(self, entity, statuses=None): log.debug(_(u"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("presenceReceived", entity, "unavailable", 0, statuses, self.parent.profile): 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.getEntityDatum(entity, "presence", self.parent.profile) 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.presenceUpdate(entity.full(), C.PRESENCE_UNAVAILABLE, 0, statuses, self.parent.profile) self.host.memory.setPresenceStatus(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.getParamA("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 self.send(presence_elt) @defer.inlineCallbacks def subscribed(self, entity): yield self.parent.roster.got_roster xmppim.PresenceClientProtocol.subscribed(self, entity) self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile) item = self.parent.roster.getItem(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.delWaitingSub(entity.userhost(), self.parent.profile) def subscribedReceived(self, entity): log.debug(_(u"subscription approved for [%s]") % entity.userhost()) self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile) def unsubscribedReceived(self, entity): log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost()) self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile) @defer.inlineCallbacks def subscribeReceived(self, entity): log.debug(_(u"subscription request from [%s]") % entity.userhost()) yield self.parent.roster.got_roster item = self.parent.roster.getItem(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.addWaitingSub('subscribe', entity.userhost(), self.parent.profile) self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile) @defer.inlineCallbacks def unsubscribeReceived(self, entity): log.debug(_(u"unsubscription asked for [%s]") % entity.userhost()) yield self.parent.roster.got_roster item = self.parent.roster.getItem(entity) if item and item.subscriptionFrom: # we automatically remove contact log.debug(_('automatic contact deletion')) self.host.delContact(entity, self.parent.profile) self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile) class SatDiscoProtocol(disco.DiscoClientProtocol): def __init__(self, host): disco.DiscoClientProtocol.__init__(self) class SatFallbackHandler(generic.FallbackHandler): def __init__(self, host): generic.FallbackHandler.__init__(self) def iqFallback(self, iq): if iq.handled is True: return log.debug(u"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) class SatIdentityHandler(XMPPHandler): """ Manage disco Identity of SàT. """ #TODO: dynamic identity update (see docstring). Note that a XMPP entity can have several identities implements(iwokkel.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return self.parent.identities def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/core/log_config.py0000644000175500017600000004435013243470024020177 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.core.constants import Const as C from sat.core import log class TwistedLogger(log.Logger): colors = True force_colors = False def __init__(self, *args, **kwargs): super(TwistedLogger, self).__init__(*args, **kwargs) from twisted.python import log as twisted_log self.twisted_log = twisted_log def out(self, message, level=None): """Actually log the message @param message: formatted message """ self.twisted_log.msg(message.encode('utf-8', 'ignore'), sat_logged=True, level=level) class ConfigureBasic(log.ConfigureBase): def configureColors(self, colors, force_colors, levels_taints_dict): super(ConfigureBasic, self).configureColors(colors, force_colors, levels_taints_dict) if colors: import sys try: isatty = sys.stdout.isatty() except AttributeError: isatty = False if force_colors or isatty: # FIXME: isatty should be tested on each handler, not globaly # we need colors log.Logger.post_treat = lambda logger, level, message: self.ansiColors(level, message) elif force_colors: raise ValueError("force_colors can't be used if colors is False") @staticmethod def getProfile(): """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 changeObserver(self, observer, can_colors=False): """Install a hook on observer to manage SàT specificities @param observer: original observer to hook @param can_colors: True if observer can display ansi colors """ def observer_hook(event): """redirect non SàT log to twisted_logger, and add colors when possible""" if 'sat_logged' in event: # we only want our own logs, other are managed by twistedObserver # we add colors if possible if (can_colors and self.LOGGER_CLASS.colors) or self.LOGGER_CLASS.force_colors: message = event.get('message', tuple()) level = event.get('level', C.LOG_LVL_INFO) if message: event['message'] = (self.ansiColors(level, ''.join(message)),) # must be a tuple observer(event) # we can now call the original observer return observer_hook def changeFileLogObserver(self, observer): """Install SàT hook for FileLogObserver if the output is a tty, we allow colors, else we don't @param observer: original observer to hook """ log_obs = observer.__self__ log_file = log_obs.write.__self__ try: can_colors = log_file.isatty() except AttributeError: can_colors = False return self.changeObserver(observer, can_colors=can_colors) def installObserverHook(self, observer): """Check observer type and install SàT hook when possible @param observer: observer to hook @return: hooked observer or original one """ if hasattr(observer, '__self__'): ori = observer if isinstance(observer.__self__, self.twisted_log.FileLogObserver): observer = self.changeFileLogObserver(observer) elif isinstance(observer.__self__, self.twisted_log.DefaultObserver): observer = self.changeObserver(observer, can_colors=True) else: # we use print because log system is not fully initialized print("Unmanaged observer [%s]" % observer) return observer self.observers[ori] = observer return observer def preTreatment(self): """initialise needed attributes, and install observers hooks""" self.observers = {} from twisted.python import log as twisted_log self.twisted_log = twisted_log self.log_publisher = twisted_log.msg.__self__ def addObserverObserver(self_logpub, other): """Install hook so we know when a new observer is added""" other = self.installObserverHook(other) return self_logpub._originalAddObserver(other) def removeObserverObserver(self_logpub, ori): """removeObserver hook fix As we wrap the original observer, the original removeObserver may want to remove the original object instead of the wrapper, this method fix this """ if ori in self.observers: self_logpub._originalRemoveObserver(self.observers[ori]) else: try: self_logpub._originalRemoveObserver(ori) except ValueError: try: ori in self.cleared_observers except AttributeError: raise ValueError("Unknown observer") # we replace addObserver/removeObserver by our own twisted_log.LogPublisher._originalAddObserver = twisted_log.LogPublisher.addObserver twisted_log.LogPublisher._originalRemoveObserver = twisted_log.LogPublisher.removeObserver import types # see https://stackoverflow.com/a/4267590 (thx Chris Morgan/aaronasterling) twisted_log.addObserver = types.MethodType(addObserverObserver, self.log_publisher, twisted_log.LogPublisher) twisted_log.removeObserver = types.MethodType(removeObserverObserver, self.log_publisher, twisted_log.LogPublisher) # we now change existing observers for idx, observer in enumerate(self.log_publisher.observers): self.log_publisher.observers[idx] = self.installObserverHook(observer) def configureLevel(self, level): self.LOGGER_CLASS.level = level super(ConfigureTwisted, self).configureLevel(level) def configureOutput(self, output): import sys if output is None: output = C.LOG_OPT_OUTPUT_SEP + C.LOG_OPT_OUTPUT_DEFAULT self.manageOutputs(output) addObserver = self.twisted_log.addObserver if C.LOG_OPT_OUTPUT_DEFAULT in log.handlers: # default output is already managed, we just add output to stdout if we are in debug or nodaemon mode if self.backend_data is None: raise ValueError("You must pass options as backend_data with Twisted backend") options = self.backend_data if options.get('nodaemon', False) or options.get('debug', False): addObserver(self.twisted_log.FileLogObserver(sys.stdout).emit) else: # \\default is not in the output, so we remove current observers self.cleared_observers = self.log_publisher.observers self.observers.clear() del self.log_publisher.observers[:] # and we forbid twistd to add any observer self.twisted_log.addObserver = lambda other: None if C.LOG_OPT_OUTPUT_FILE in log.handlers: from twisted.python import logfile for path in log.handlers[C.LOG_OPT_OUTPUT_FILE]: log_file = sys.stdout if path == '-' else logfile.LogFile.fromFullPath(path) addObserver(self.twisted_log.FileLogObserver(log_file).emit) if C.LOG_OPT_OUTPUT_MEMORY in log.handlers: raise NotImplementedError("Memory observer is not implemented in Twisted backend") def configureColors(self, colors, force_colors, levels_taints_dict): super(ConfigureTwisted, self).configureColors(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 postTreatment(self): """Install twistedObserver which manage non SàT logs""" def twistedObserver(event): """Observer which redirect log message not produced by SàT to SàT logging system""" if not 'sat_logged' in event: # this log was not produced by SàT from twisted.python import log as twisted_log text = twisted_log.textFromEventDict(event) if text is None: return twisted_logger = log.getLogger(C.LOG_TWISTED_LOGGER) log_method = twisted_logger.error if event.get('isError', False) else twisted_logger.info log_method(text.decode('utf-8')) self.log_publisher._originalAddObserver(twistedObserver) 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 preTreatment(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 configureLevel(self, level): if level is None: level = C.LOG_LVL_DEBUG self.level = level def configureFormat(self, fmt): super(ConfigureStandard, self).configureFormat(fmt) import logging import sys class SatFormatter(logging.Formatter): u"""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.getProfile() 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 ansiColors 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.ansiColors(record.levelname, s) if sys.platform == "android": # FIXME: dirty hack to workaround android encoding issue on log # need to be fixed properly return s.encode('ascii', 'ignore') else: return s self.formatterClass = SatFormatter def configureOutput(self, output): self.manageOutputs(output) def configureLogger(self, logger): self.name_filter = log.FilterName(logger) if logger else None def configureColors(self, colors, force_colors, levels_taints_dict): super(ConfigureStandard, self).configureColors(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 _addHandler(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 postTreatment(self): import logging root_logger = logging.getLogger() if len(root_logger.handlers) == 0: for handler, options in 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._addHandler(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._addHandler(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._addHandler(root_logger, hdlr, can_colors=False) else: raise ValueError("Unknown handler type") else: root_logger.warning(u"Handlers already set on root logger") @staticmethod def memoryGet(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 _parseOptions(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 satConfigure(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 sat.tools import config import os log_conf = {} sat_conf = config.parseMainConf() 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.getConfig(sat_conf, C.LOG_OPT_SECTION, C.LOG_OPT_PREFIX + opt_name, opt_default) _parseOptions(log_conf) configure(backend, backend_data=backend_data, **log_conf) sat-0.6.1.1+hg20180208/src/core/exceptions.py0000644000175500017600000000431713243470024020251 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # 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) """ pass class ProfileConnected(Exception): """This error is raised when trying to delete a connected profile.""" pass class ProfileNotConnected(Exception): pass class ProfileKeyUnknown(Exception): pass 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 NotFound(Exception): pass class DataError(Exception): pass 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(Exception): pass # Something which need to be done is not available yet class NotReady(Exception): pass sat-0.6.1.1+hg20180208/src/core/constants.py0000644000175500017600000002677513243470024020120 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 class Const(object): ## Application ## APP_NAME = u'Salut à Toi' APP_NAME_SHORT = u'SàT' APP_NAME_FILE = u'sat' APP_NAME_FULL = u'%s (%s)' % (APP_NAME_SHORT, APP_NAME) APP_VERSION = u'0.7.0D' # Please add 'D' at the end for dev versions APP_RELEASE_NAME = u'La Commune' APP_URL = u'http://salut-a-toi.org' ## Runtime ## PLUGIN_EXT = "py" HISTORY_SKIP = u'skip' ## Main config ## DEFAULT_BRIDGE = 'dbus' ## Protocol ## XMPP_C2S_PORT = 5222 XMPP_KEEP_ALIFE = 180 XMPP_MAX_RETRIES = 2 # 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' ## 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" ## 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_FORWARD = 'urn:xmpp:forward:0' NS_DELAY = 'urn:xmpp:delay' NS_XHTML = 'http://www.w3.org/1999/xhtml' ## Directories ## CACHE_DIR = u'cache' ## Configuration ## if BaseDirectory: # skipped when xdg module is not available (should not happen in backend) if "org.goffi.cagou.cagou" in BaseDirectory.__file__: # FIXME: hack to make config read from the right location on Android # TODO: fix it in a more proper way BaseDirectory = None DEFAULT_CONFIG = { 'local_dir': '/data/data/org.goffi.cagou.cagou/', 'media_dir': '/data/data/org.goffi.cagou.cagou/files/media', 'pid_dir': '%(local_dir)s', 'log_dir': '%(local_dir)s', } CONFIG_FILES = ['/data/data/org.goffi.cagou.cagou/files/platform/android/' + APP_NAME_FILE + '.conf'] else: ## Configuration ## DEFAULT_CONFIG = { 'media_dir': '/usr/share/' + APP_NAME_FILE + '/media', 'local_dir': BaseDirectory.save_data_path(APP_NAME_FILE), 'pid_dir': '%(local_dir)s', 'log_dir': '%(local_dir)s', } # List of the configuration filenames sorted by ascending priority CONFIG_FILES = [realpath(expanduser(path) + APP_NAME_FILE + '.conf') for path in ['/etc/', '~/', '~/.', '', '.'] + ['%s/' % path for path in list(BaseDirectory.load_config_paths(APP_NAME_FILE))] ] ## Templates ## TEMPLATE_THEME_DEFAULT = u'default' TEMPLATE_STATIC_DIR = u'static' ## 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 = u'name' PI_IMPORT_NAME = u'import_name' PI_MAIN = u'main' PI_HANDLER = u'handler' PI_TYPE = u'type' # FIXME: should be types, and should handle single unicode type or tuple of types (e.g. "blog" and "import") PI_MODES = u'modes' PI_PROTOCOLS = u'protocols' PI_DEPENDENCIES = u'dependencies' PI_RECOMMENDATIONS = u'recommendations' PI_DESCRIPTION = u'description' PI_USAGE = u'usage' # Types PLUG_TYPE_XEP = "XEP" PLUG_TYPE_MISC = "MISC" PLUG_TYPE_EXP = "EXP" PLUG_TYPE_SEC = "SEC" PLUG_TYPE_SYNTAXE = "SYNTAXE" 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,) # 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_ITEM = "item" PS_ITEMS = "items" # Can contain publish and retract items PS_EVENTS = (PS_ITEMS, PS_DELETE) ## 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 = "info" XMLUI_DATA_LVL_WARNING = "warning" XMLUI_DATA_LVL_ERROR = "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 = 50 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_OVERWRITE = "overwrite" ## HARD-CODED ACTIONS IDS (generated with uuid.uuid4) ## AUTHENTICATE_PROFILE_ID = u'b03bbfa8-a4ae-4734-a248-06ce6c7cf562' CHANGE_XMPP_PASSWD_ID = u'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 ## 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 HASH_SHA1_EMPTY = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' @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): """@return (bool): bool value for associated constant""" assert isinstance(value, basestring) return value.lower() in (cls.BOOL_TRUE, "1") @classmethod def boolConst(cls, value): """@return (str): constant associated to bool value""" assert isinstance(value, bool) return cls.BOOL_TRUE if value else cls.BOOL_FALSE sat-0.6.1.1+hg20180208/src/core/log.py0000644000175500017600000003302013243470024016642 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.core.constants import Const as C from sat.tools.common.ansi import ANSI as A from sat.core import exceptions backend = None _loggers = {} handlers = {} COLOR_START = '%(color_start)s' COLOR_END = '%(color_end)s' class Filtered(Exception): pass class Logger(object): """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 out(self, message, level=None): """Actually log the message @param message: formatted message """ print message def log(self, level, message): """Print message @param level: one of C.LOG_LEVELS @param message: message to format and print """ try: formatted = self.format(level, message) if self.post_treat is None: self.out(formatted, level) else: self.out(self.post_treat(level, formatted), level) except Filtered: pass def format(self, level, message): """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.dictFilter(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].getProfile() return self.fmt % record else: raise e def debug(self, msg): self.log(C.LOG_LVL_DEBUG, msg) def info(self, msg): self.log(C.LOG_LVL_INFO, msg) def warning(self, msg): self.log(C.LOG_LVL_WARNING, msg) def error(self, msg): self.log(C.LOG_LVL_ERROR, msg) def critical(self, msg): self.log(C.LOG_LVL_CRITICAL, msg) 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 dictFilter(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(object): LOGGER_CLASS = Logger _color_location = False # True if color location is specified in fmt (with COLOR_START) 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.preTreatment() self.configureLevel(level) self.configureFormat(fmt) self.configureOutput(output) self.configureLogger(logger) self.configureColors(colors, force_colors, levels_taints_dict) self.postTreatment() self.updateCurrentLogger() def updateCurrentLogger(self): """update existing logger to the class needed for this backend""" if self.LOGGER_CLASS is None: return for name, logger in _loggers.items(): _loggers[name] = self.LOGGER_CLASS(logger) def preTreatment(self): pass def configureLevel(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 configureFormat(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 configureOutput(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 configureLogger(self, logger): if logger: Logger.filter_name = FilterName(logger) def configureColors(self, colors, force_colors, levels_taints_dict): if colors: # if color are used, we need to handle levels_taints_dict for level in 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 postTreatment(self): pass def manageOutputs(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(u"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(u"options [{options}] are not supported for {handler} output".format(options=options, handler=output)) @staticmethod def memoryGet(size=None): """Return buffered logs @param size: number of logs to return """ raise NotImplementedError @classmethod def ansiColors(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 getProfile(): """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 memoryGet(size=None): if not C.LOG_OPT_OUTPUT_MEMORY in handlers: raise ValueError('memory output is not used') return configure_cls[backend].memoryGet(size) def getLogger(name=C.LOG_BASE_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): _root_logger.debug(msg) def info(msg): _root_logger.info(msg) def warning(msg): _root_logger.warning(msg) def error(msg): _root_logger.error(msg) def critical(msg): _root_logger.critical(msg) sat-0.6.1.1+hg20180208/src/core/i18n.py0000644000175500017600000000264213243470024016646 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.log import getLogger log = getLogger(__name__) try: import gettext _ = gettext.translation('sat', 'i18n', fallback=True).ugettext _translators = {None: gettext.NullTranslations()} def languageSwitch(lang=None): if not lang in _translators: _translators[lang] = gettext.translation('sat', languages=[lang], fallback=True) _translators[lang].install(unicode=True) except ImportError: log.warning("gettext support disabled") _ = lambda msg: msg # Libervia doesn't support gettext def languageSwitch(lang=None): pass D_ = lambda msg: msg # used for deferred translations sat-0.6.1.1+hg20180208/src/core/__init__.py0000644000175500017600000000000013243470024017610 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/core/sat_main.py0000644000175500017600000012662713243470024017674 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat from sat.core.i18n import _, languageSwitch 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 sat.core import xmpp from sat.core import exceptions from sat.core.log import getLogger log = getLogger(__name__) from sat.core.constants import Const as C from sat.memory.memory import Memory from sat.tools import trigger from sat.tools import utils from sat.tools.common import dynamic_import from sat.stdui import ui_contact_list, ui_profile_manager import sat.plugins from glob import glob import sys import os.path import uuid try: from collections import OrderedDict # only available from python 2.7 except ImportError: from ordereddict import OrderedDict class SAT(service.Service): def __init__(self): self._cb_map = {} # map from callback_id to callbacks self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary) 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 = {} self._ns_map = {u'x-data': u'jabber:x:data'} # map for short name to whole namespace, # extended by plugins with registerNamespace self.memory = Memory(self) self.trigger = trigger.TriggerManager() # trigger are used to change SàT behaviour bridge_name = self.memory.getConfig('', 'bridge', 'dbus') bridge_module = dynamic_import.bridge(bridge_name) if bridge_module is None: log.error(u"Can't find bridge module of name {}".format(bridge_name)) sys.exit(1) log.info(u"using {} bridge".format(bridge_name)) try: self.bridge = bridge_module.Bridge() except exceptions.BridgeInitError: log.error(u"Bridge can't be initialised, can't start SàT core") sys.exit(1) self.bridge.register_method("getReady", lambda: self.initialised) self.bridge.register_method("getVersion", lambda: self.full_version) self.bridge.register_method("getFeatures", self.getFeatures) self.bridge.register_method("profileNameGet", self.memory.getProfileName) self.bridge.register_method("profilesListGet", self.memory.getProfilesList) self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile)) self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData) self.bridge.register_method("profileCreate", self.memory.createProfile) self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile) self.bridge.register_method("profileStartSession", self.memory.startSession) self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted) self.bridge.register_method("profileSetDefault", self.memory.profileSetDefault) self.bridge.register_method("connect", self._connect) self.bridge.register_method("disconnect", self.disconnect) self.bridge.register_method("getContacts", self.getContacts) self.bridge.register_method("getContactsFromGroup", self.getContactsFromGroup) self.bridge.register_method("getMainResource", self.memory._getMainResource) self.bridge.register_method("getPresenceStatuses", self.memory._getPresenceStatuses) self.bridge.register_method("getWaitingSub", self.memory.getWaitingSub) self.bridge.register_method("messageSend", self._messageSend) self.bridge.register_method("getConfig", self._getConfig) self.bridge.register_method("setParam", self.setParam) self.bridge.register_method("getParamA", self.memory.getStringParamA) self.bridge.register_method("asyncGetParamA", self.memory.asyncGetStringParamA) self.bridge.register_method("asyncGetParamsValuesFromCategory", self.memory.asyncGetParamsValuesFromCategory) self.bridge.register_method("getParamsUI", self.memory.getParamsUI) self.bridge.register_method("getParamsCategories", self.memory.getParamsCategories) self.bridge.register_method("paramsRegisterApp", self.memory.paramsRegisterApp) self.bridge.register_method("historyGet", self.memory._historyGet) self.bridge.register_method("setPresence", self._setPresence) self.bridge.register_method("subscription", self.subscription) self.bridge.register_method("addContact", self._addContact) self.bridge.register_method("updateContact", self._updateContact) self.bridge.register_method("delContact", self._delContact) self.bridge.register_method("isConnected", self.isConnected) self.bridge.register_method("launchAction", self.launchCallback) self.bridge.register_method("actionsGet", self.actionsGet) self.bridge.register_method("progressGet", self._progressGet) self.bridge.register_method("progressGetAll", self._progressGetAll) self.bridge.register_method("menusGet", self.getMenus) self.bridge.register_method("menuHelpGet", self.getMenuHelp) self.bridge.register_method("menuLaunch", self._launchMenu) self.bridge.register_method("discoInfos", self.memory.disco._discoInfos) self.bridge.register_method("discoItems", self.memory.disco._discoItems) self.bridge.register_method("saveParamsTemplate", self.memory.save_xml) self.bridge.register_method("loadParamsTemplate", self.memory.load_xml) self.bridge.register_method("sessionInfosGet", self.getSessionInfos) self.bridge.register_method("namespacesGet", self.getNamespaces) self.memory.initialized.addCallback(self._postMemoryInit) @property def version(self): """Return the short version of SàT""" return C.APP_VERSION @property def full_version(self): """Return the full version of SàT (with release name and extra data when in development mode)""" 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 = u"{} « {} » ({})".format(version, C.APP_RELEASE_NAME, utils.getRepositoryData(sat)) return self._version_cache else: return version @property def bridge_name(self): return os.path.splitext(os.path.basename(self.bridge.__file__))[0] def _postMemoryInit(self, ignore): """Method called after memory initialization is done""" log.info(_("Memory initialised")) try: self._import_plugins() ui_contact_list.ContactList(self) ui_profile_manager.ProfileManager(self) except Exception as e: log.error(_(u"Could not initialize backend: {reason}").format( reason = str(e).decode('utf-8', 'ignore'))) sys.exit(1) self.initialised.callback(None) log.info(_(u"Backend is ready")) 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 sat.conf plugins_path = os.path.dirname(sat.plugins.__file__) plugin_glob = "plugin*." + C.PLUGIN_EXT plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))] plugins_to_import = {} # plugins we still have to import for plug in plug_lst: plugin_path = 'sat.plugins.' + plug try: __import__(plugin_path) except exceptions.MissingModule as e: self._unimport_plugin(plugin_path) log.warning(u"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(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e)) self._unimport_plugin(plugin_path) continue except Exception as e: import traceback log.error(_(u"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[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT)) # if the plugin is an entry point, it must work in component mode if plugin_info[u'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(_(u"{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(_(u"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(u'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(_(u"Recommended plugin not found: {}").format(import_name)) return msg = u"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(u'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(_(u"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.warning(u'Error while loading plugin "{name}", ignoring it: {error}' .format(name=plugin_info['name'], error=e)) if optional: return raise ImportError(u"Error during initiation") if 'handler' in plugin_info and plugin_info['handler'] == 'yes': 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 pluginsUnload(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.itervalues(): try: unload = plugin.unload except AttributeError: continue else: defers_list.append(defer.maybeDeferred(unload)) return defers_list def _connect(self, profile_key, password='', options=None): profile = self.memory.getProfileName(profile_key) return self.connect(profile, password, options) 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 SàT 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={} def connectProfile(dummy=None): if self.isConnected(profile): log.info(_("already connected !")) return True if self.memory.isComponent(profile): d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries) else: d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries) return d.addCallback(lambda dummy: False) d = self.memory.startSession(password, profile) d.addCallback(connectProfile) return d 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.isConnected(profile_key): # isConnected is checked here and not on client # because client is deleted when session is ended log.info(_(u"not connected !")) return defer.succeed(None) client = self.getClient(profile_key) return client.entityDisconnect() def getFeatures(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.getClient(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.iteritems(): try: features_d = defer.maybeDeferred(plugin.getFeatures, profile_key) except AttributeError: features_d = defer.succeed({}) features.append(features_d) d_list = defer.DeferredList(features) def buildFeatures(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(u"Error while getting features for {name}: {failure}".format( name=name, failure=data)) ret[name] = {} return ret d_list.addCallback(buildFeatures, self.plugins.keys()) return d_list def getContacts(self, profile_key): client = self.getClient(profile_key) def got_roster(dummy): ret = [] for item in client.roster.getItems(): # we get all items for client's roster # and convert them to expected format attr = client.roster.getAttributes(item) ret.append([item.jid.userhost(), attr, item.groups]) return ret return client.roster.got_roster.addCallback(got_roster) def getContactsFromGroup(self, group, profile_key): client = self.getClient(profile_key) return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)] def purgeEntity(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.purgeProfileSession(profile) def startService(self): log.info(u"Salut à toi ô mon frère !") def stopService(self): log.info(u"Salut aussi à Rantanplan") return self.pluginsUnload() def run(self): log.debug(_("running app")) reactor.run() def stop(self): log.debug(_("stopping app")) reactor.stop() ## Misc methods ## def getJidNStream(self, profile_key): """Convenient method to get jid and stream from profile key @return: tuple (jid, xmlstream) from profile, can be None""" # TODO: deprecate this method (getClient is enough) profile = self.memory.getProfileName(profile_key) if not profile or not self.profiles[profile].isConnected(): return (None, None) return (self.profiles[profile].jid, self.profiles[profile].xmlstream) def getClient(self, profile_key): """Convenient method to get client from profile key @return: client or None if it doesn't exist @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.getProfileName(profile_key) if not profile: raise exceptions.ProfileKeyUnknown try: return self.profiles[profile] except KeyError: raise exceptions.NotFound(profile_key) def getClients(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(_(u'profile_key must not be empty')) try: profile = self.memory.getProfileName(profile_key, True) except exceptions.ProfileUnknownError: return [] if profile == C.PROF_KEY_ALL: return self.profiles.values() elif profile[0] == '@': # only profile keys can start with "@" raise exceptions.ProfileKeyUnknown return [self.profiles[profile]] def _getConfig(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 unicode(self.memory.getConfig(section, name, '')) def logErrback(self, failure_): """generic errback logging can be used as last errback to show unexpected error """ log.error(_(u"Unexpected error: {}".format(failure_))) return failure_ ## Client management ## def setParam(self, name, value, category, security_limit, profile_key): """set wanted paramater and notice observers""" self.memory.setParam(name, value, category, security_limit, profile_key) def isConnected(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.getProfileName(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].isConnected() ## XMPP methods ## def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE): client = self.getClient(profile_key) to_jid = jid.JID(to_jid_s) #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}) def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) def setPresence(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.getProfileName(profile_key) assert profile priority = int(self.memory.getParamA("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.presenceUpdate(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.getProfileName(profile_key) assert profile to_jid = jid.JID(raw_jid) log.debug(_(u'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 _addContact(self, to_jid_s, profile_key): return self.addContact(jid.JID(to_jid_s), profile_key) def addContact(self, to_jid, profile_key): """Add a contact in roster list""" profile = self.memory.getProfileName(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 _updateContact(self, to_jid_s, name, groups, profile_key): return self.updateContact(jid.JID(to_jid_s), name, groups, profile_key) def updateContact(self, to_jid, name, groups, profile_key): """update a contact in roster list""" profile = self.memory.getProfileName(profile_key) assert profile groups = set(groups) roster_item = RosterItem(to_jid) roster_item.name = name or None roster_item.groups = set(groups) return self.profiles[profile].roster.setItem(roster_item) def _delContact(self, to_jid_s, profile_key): return self.delContact(jid.JID(to_jid_s), profile_key) def delContact(self, to_jid, profile_key): """Remove contact from roster list""" profile = self.memory.getProfileName(profile_key) assert profile self.profiles[profile].presence.unsubscribe(to_jid) # is not asynchronous return self.profiles[profile].roster.removeItem(to_jid) ## 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 checkFeature(self, *args, **kwargs): return self.memory.disco.checkFeature(*args, **kwargs) def checkFeatures(self, *args, **kwargs): return self.memory.disco.checkFeatures(*args, **kwargs) def getDiscoInfos(self, *args, **kwargs): return self.memory.disco.getInfos(*args, **kwargs) def getDiscoItems(self, *args, **kwargs): return self.memory.disco.getItems(*args, **kwargs) def findServiceEntity(self, *args, **kwargs): return self.memory.disco.findServiceEntity(*args, **kwargs) def findServiceEntities(self, *args, **kwargs): return self.memory.disco.findServiceEntities(*args, **kwargs) def findFeaturesSet(self, *args, **kwargs): return self.memory.disco.findFeaturesSet(*args, **kwargs) ## Generic HMI ## def _killAction(self, keep_id, client): log.debug(u"Killing action {} for timeout".format(keep_id)) client.actions[keep_id] def actionNew(self, action_data, security_limit=C.NO_SECURITY_LIMIT, keep_id=None, profile=C.PROF_KEY_NONE): """Shortcut to bridge.actionNew which generate and 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 must be set to the callback_id action will be deleted after 30 min. @param profile: %(doc_profile)s """ id_ = unicode(uuid.uuid4()) if keep_id is not None: client = self.getClient(profile) action_timer = reactor.callLater(60*30, self._killAction, keep_id, client) client.actions[keep_id] = (action_data, id_, security_limit, action_timer) self.bridge.actionNew(action_data, id_, security_limit, profile) def actionsGet(self, profile): """Return current non answered actions @param profile: %(doc_profile)s """ client = self.getClient(profile) return [action_tuple[:-1] for action_tuple in client.actions.itervalues()] def registerProgressCb(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.getClient(profile) if progress_id in client._progress_cb: raise exceptions.ConflictError(u"Progress ID is not unique !") client._progress_cb[progress_id] = (callback, metadata) def removeProgressCb(self, progress_id, profile): """Remove a progress callback""" client = self.getClient(profile) try: del client._progress_cb[progress_id] except KeyError: log.error(_(u"Trying to remove an unknow progress callback")) def _progressGet(self, progress_id, profile): data = self.progressGet(progress_id, profile) return {k: unicode(v) for k,v in data.iteritems()} def progressGet(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.getClient(profile) try: data = client._progress_cb[progress_id][0](progress_id, profile) except KeyError: data = {} return data def _progressGetAll(self, profile_key): progress_all = self.progressGetAll(profile_key) for profile, progress_dict in progress_all.iteritems(): for progress_id, data in progress_dict.iteritems(): for key, value in data.iteritems(): data[key] = unicode(value) return progress_all def progressGetAllMetadata(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 [progressStarted] """ clients = self.getClients(profile_key) progress_all = {} for client in clients: profile = client.profile progress_dict = {} progress_all[profile] = progress_dict for progress_id, (dummy, progress_metadata) in client._progress_cb.iteritems(): progress_dict[progress_id] = progress_metadata return progress_all def progressGetAll(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 [progressGet] """ clients = self.getClients(profile_key) progress_all = {} for client in clients: profile = client.profile progress_dict = {} progress_all[profile] = progress_dict for progress_id, (progress_cb, dummy) in client._progress_cb.iteritems(): progress_dict[progress_id] = progress_cb(progress_id, profile) return progress_all def registerCallback(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 have 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(_(u"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 purgeCallback(): try: self.removeCallback(callback_id) except KeyError: pass reactor.callLater(1800, purgeCallback) return callback_id def removeCallback(self, callback_id): """ Remove a previously registered callback @param callback_id: id returned by [registerCallback] """ log.debug("Removing callback [%s]" % callback_id) del self._cb_map[callback_id] def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE): """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: security limit need to be checked here try: client = self.getClient(profile_key) except exceptions.NotFound: # client is not available yet profile = self.memory.getProfileName(profile_key) if not profile: raise exceptions.ProfileUnknownError(_(u'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(u"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.removeCallback(callback_id) return defer.maybeDeferred(callback, *args, **kwargs) #Menus management def _getMenuCanonicalPath(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 importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL): """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 [registerCallback] @param security_limit(int): %(doc_security_limit)s /!\ security_limit MUST be added to data in launchCallback 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.registerCallback(callback, with_data=True) elif isinstance(callback, basestring): # 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.itervalues(): 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._getMenuCanonicalPath(path) menu_key = (type_, path_canonical) if menu_key in self._menus_paths: raise exceptions.ConflictError(u"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 getMenus(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.iteritems(): 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 languageSwitch(language) path_i18n = [_(elt) for elt in path] languageSwitch() extra = {} # TODO: manage extra data like icon ret.append((menu_id, type_, path, path_i18n, extra)) return ret def _launchMenu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): client = self.getClient(profile_key) return self.launchMenu(client, menu_type, path, data, security_limit) def launchMenu(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._getMenuCanonicalPath(path) menu_key = (menu_type, canonical_path) try: callback_id = self._menus_paths[menu_key] except KeyError: raise exceptions.NotFound(u"Can't find menu {path} ({menu_type})".format( path=canonical_path, menu_type=menu_type)) return self.launchCallback(callback_id, data, client.profile) def getMenuHelp(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") languageSwitch(language) help_string = _(menu_data['help_string']) languageSwitch() return help_string # misc methods def registerNamespace(self, short_name, namespace): """associate a namespace to a short name""" if short_name in self._ns_map: raise exceptions.ConflictError(u'this short name is already used') self._ns_map[short_name] = namespace def getNamespaces(self): return self._ns_map def getSessionInfos(self, profile_key): """compile interesting data on current profile session""" client = self.getClient(profile_key) data = { "jid": client.jid.full(), "started": unicode(int(client.started)), } return defer.succeed(data) sat-0.6.1.1+hg20180208/src/twisted/0002755000175500017600000000000013243470025016247 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/twisted/plugins/0002755000175500017600000000000013243470025017730 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/twisted/plugins/sat_plugin.py0000644000175500017600000000431113243470025022444 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 twisted.internet import defer if defer.Deferred.debug: # if we are in debug mode, we want to use ipdb instead of pdb try: import ipdb import pdb pdb.set_trace = ipdb.set_trace pdb.post_mortem = ipdb.post_mortem except ImportError: pass from zope.interface import implements from twisted.python import usage from twisted.plugin import IPlugin from twisted.application.service import IServiceMaker # XXX: We need to configure logs before any log method is used, so here is the best place. from sat.core.constants import Const as C from sat.core.i18n import _ def initialise(options): """Method to initialise global modules""" from twisted.internet import glib2reactor glib2reactor.install() # XXX: We need to configure logs before any log method is used, so here is the best place. from sat.core import log_config log_config.satConfigure(C.LOG_BACKEND_TWISTED, C, backend_data=options) class Options(usage.Options): optParameters = [] class SatMaker(object): implements(IServiceMaker, IPlugin) tapname = C.APP_NAME_FILE description = _(u"%s XMPP client backend") % C.APP_NAME_FULL options = Options def makeService(self, options): # XXX: SAT must be imported after log configuration, because it write stuff to logs initialise(options.parent) from sat.core.sat_main import SAT return SAT() serviceMaker = SatMaker() sat-0.6.1.1+hg20180208/src/bridge/0002755000175500017600000000000013243470024016017 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/0002755000175500017600000000000013243470024021720 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/base_constructor.py0000644000175500017600000003314313243470024025653 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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 need 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 getDefault(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 getArgumentsDoc(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 getDoc(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 argumentsParser(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 getArguments(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 getArgumentsDoc @param default: dictionary of default values, like given by getDefault @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.argumentsParser(signature): attr_string.append(("unicode(%(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 getTemplatePath(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 generateCoreSide or generateFrontendSide if they exists else call generic self._generate method """ try: if side == "core": method = self.generateCoreSide elif side == "frontend": if not self.FRONTEND_ACTIVATE: print(u"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.getDefault(section) arg_doc = self.getArgumentsDoc(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.getArguments(function['sig_in'], name=arg_doc, default=default), } extend_method = getattr(self, "{}_completion_{}".format(side, function["type"])) extend_method(completion, function, default, arg_doc, async_) for part, fmt in FORMATS.iteritems(): if part.startswith(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.iteritems() if env.startswith(C.ENV_OVERRIDE)} template_path = self.getTemplatePath(TEMPLATE) try: with open(template_path) as template: for line in template: for part, extend_list in parts.iteritems(): 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.finalWrite(DEST, bridge) def finalWrite(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) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/0002755000175500017600000000000013243470024024470 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/pb/0002755000175500017600000000000013243470024025071 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/pb/__init__.py0000644000175500017600000000000013243470024027166 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/pb/constructor.py0000644000175500017600000000446313243470024030035 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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.sendSignal("{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: errback = self._generic_errback d.addErrback(errback)\n""", } def core_completion_signal(self, completion, function, default, arg_doc, async_): completion['args_no_def'] = self.getArguments(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.getArguments(function['sig_in'], name=arg_doc), 'callback': 'callback' if function['sig_out'] else 'lambda dummy: callback()', 'debug': "" if not self.args.debug else 'log.debug ("%s")\n%s' % (completion['name'], 8 * ' '), }) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/pb/pb_frontend_template.py0000644000175500017600000001022013243470024031627 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SAT communication bridge # Copyright (C) 2009-2018 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 sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from twisted.spread import pb from twisted.internet import reactor class SignalsHandler(pb.Referenceable): def __getattr__(self, name): if name.startswith("remote_"): log.debug(u"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__(self, method_name) except AttributeError: pass else: raise exceptions.InternalError(u"{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 lambda *args, **kwargs: self.call(name, args, kwargs) def remoteCallback(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.remoteCallback, callback) if errback is not None: d.addErrback(errback) def _initBridgeEb(self, failure): log.error(u"Can't init bridge: {msg}".format(msg=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._initBridgeEb) return d def _generic_errback(self, failure): log.error(u"bridge failure: {}".format(failure)) def bridgeConnect(self, callback, errback): factory = pb.PBClientFactory() reactor.connectTCP("localhost", 8789, factory) d = factory.getRootObject() d.addCallback(self._set_root) d.addCallback(lambda dummy: callback()) d.addErrback(errback) def register_signal(self, functionName, handler, iface="core"): self.signals_handler.register_signal(functionName, handler, iface) ##METHODS_PART## sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/pb/pb_core_template.py0000644000175500017600000000662213243470024030753 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.log import getLogger log = getLogger(__name__) from twisted.spread import jelly, pb from twisted.internet import reactor ## 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 class PBRoot(pb.Root): def __init__(self): self.signals_handlers = [] def remote_initBridge(self, signals_handler): self.signals_handlers.append(signals_handler) log.info(u"registered signal handler") def sendSignalEb(self, failure, signal_name): log.error(u"Error while sending signal {name}: {msg}".format( name = signal_name, msg = failure, )) def sendSignal(self, name, args, kwargs): to_remove = [] for handler in self.signals_handlers: try: d = handler.callRemote(name, *args, **kwargs) except pb.DeadReferenceError: to_remove.append(handler) else: d.addErrback(self.sendSignalEb, name) if to_remove: for handler in to_remove: log.debug(u"Removing signal handler for dead frontend") self.signals_handlers.remove(handler) ##METHODS_PART## class Bridge(object): def __init__(self): log.info("Init Perspective Broker...") self.root = PBRoot() reactor.listenTCP(8789, pb.PBServerFactory(self.root)) def sendSignal(self, name, *args, **kwargs): self.root.sendSignal(name, args, kwargs) def remote_initBridge(self, signals_handler): self.signals_handlers.append(signals_handler) log.info(u"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 addMethod(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 addSignal(self, name, int_suffix, signature, doc={}): log.debug("Adding signal {name} to PB bridge".format(name=name)) setattr(self, name, lambda *args, **kwargs: self.sendSignal(name, *args, **kwargs)) ##SIGNALS_PART## sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus/0002755000175500017600000000000013243470024025425 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus/dbus_core_template.py0000644000175500017600000002376713243470024031654 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.i18n import _ import dbus import dbus.service import dbus.mainloop.glib import inspect from sat.core.log import getLogger log = getLogger(__name__) from twisted.internet.defer import Deferred from sat.core.exceptions import BridgeInitError const_INT_PREFIX = "org.goffi.SAT" # Interface prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = '/org/goffi/SAT/bridge' const_CORE_SUFFIX = ".core" const_PLUGIN_SUFFIX = ".plugin" class ParseError(Exception): pass class MethodNotRegistered(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".MethodNotRegistered" class InternalError(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".InternalError" class AsyncNotDeferred(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".AsyncNotDeferred" class DeferredNotAsync(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".DeferredNotAsync" class GenericException(dbus.DBusException): def __init__(self, twisted_error): """ @param twisted_error (Failure): instance of twisted Failure @return: DBusException """ super(GenericException, self).__init__() 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__ message = twisted_error.getErrorMessage() try: self.args = (message, twisted_error.value.condition) except AttributeError: self.args = (message,) self._dbus_error_name = '.'.join([const_ERROR_PREFIX, class_.__module__, class_.__name__]) class DbusObject(dbus.service.Object): def __init__(self, bus, path): dbus.service.Object.__init__(self, bus, 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 if the callback return a deferred, use async methods""" if not name in self.cb: raise MethodNotRegistered if "callback" in kwargs: #we must have errback too if not "errback" in kwargs: log.error("errback is missing in method call [%s]" % name) raise InternalError callback = kwargs.pop("callback") errback = kwargs.pop("errback") async = True else: async = False result = self.cb[name](*args, **kwargs) if async: if not isinstance(result, Deferred): log.error("Asynchronous method [%s] does not return a Deferred." % name) raise AsyncNotDeferred result.addCallback(lambda result: callback() if result is None else callback(result)) result.addErrback(lambda err: errback(GenericException(err))) else: if isinstance(result, Deferred): log.error("Synchronous method [%s] return a Deferred." % name) raise DeferredNotAsync return result ### signals ### @dbus.service.signal(const_INT_PREFIX + const_PLUGIN_SUFFIX, signature='') def dummySignal(self): #FIXME: workaround for addSignal (doesn't work if one method doensn't # already exist for plugins), probably missing some initialisation, need # further investigations pass ##SIGNALS_PART## ### methods ### ##METHODS_PART## def __attributes(self, in_sign): """Return arguments to user given a in_sign @param in_sign: in_sign in the short form (using s,a,i,b etc) @return: list of arguments that correspond to a in_sign (e.g.: "sss" return "arg1, arg2, arg3")""" i = 0 idx = 0 attr = [] while i < len(in_sign): if in_sign[i] not in ['b', 'y', 'n', 'i', 'x', 'q', 'u', 't', 'd', 's', 'a']: raise ParseError("Unmanaged attribute type [%c]" % in_sign[i]) attr.append("arg_%i" % idx) idx += 1 if in_sign[i] == 'a': i += 1 if in_sign[i] != '{' and in_sign[i] != '(': # FIXME: must manage tuples out of arrays i += 1 continue # we have a simple type for the array opening_car = in_sign[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(in_sign): raise ParseError("missing }") if in_sign[i] == opening_car: opening_count += 1 if in_sign[i] == closing_car: opening_count -= 1 if opening_count == 0: break i += 1 return attr def addMethod(self, name, int_suffix, in_sign, out_sign, method, async=False): """Dynamically add a method to Dbus Bridge""" inspect_args = inspect.getargspec(method) _arguments = inspect_args.args _defaults = list(inspect_args.defaults or []) if inspect.ismethod(method): #if we have a method, we don't want the first argument (usually 'self') del(_arguments[0]) #first arguments are for the _callback method arguments_callback = ', '.join([repr(name)] + ((_arguments + ['callback=callback', 'errback=errback']) if async else _arguments)) if async: _arguments.extend(['callback', 'errback']) _defaults.extend([None, None]) #now we create a second list with default values for i in range(1, len(_defaults) + 1): _arguments[-i] = "%s = %s" % (_arguments[-i], repr(_defaults[-i])) arguments_defaults = ', '.join(_arguments) code = compile('def %(name)s (self,%(arguments_defaults)s): return self._callback(%(arguments_callback)s)' % {'name': name, 'arguments_defaults': arguments_defaults, 'arguments_callback': arguments_callback}, '', 'exec') exec (code) # FIXME: to the same thing in a cleaner way, without compile/exec method = locals()[name] async_callbacks = ('callback', 'errback') if async else None setattr(DbusObject, name, dbus.service.method( const_INT_PREFIX + int_suffix, in_signature=in_sign, out_signature=out_sign, async_callbacks=async_callbacks)(method)) function = getattr(self, name) func_table = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__][function._dbus_interface] func_table[function.__name__] = function # Needed for introspection def addSignal(self, name, int_suffix, signature, doc={}): """Dynamically add a signal to Dbus Bridge""" attributes = ', '.join(self.__attributes(signature)) #TODO: use doc parameter to name attributes #code = compile ('def '+name+' (self,'+attributes+'): log.debug ("'+name+' signal")', '','exec') #XXX: the log.debug is too annoying with xmllog code = compile('def ' + name + ' (self,' + attributes + '): pass', '', 'exec') exec (code) signal = locals()[name] setattr(DbusObject, name, dbus.service.signal( const_INT_PREFIX + int_suffix, signature=signature)(signal)) function = getattr(self, name) func_table = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__][function._dbus_interface] func_table[function.__name__] = function # Needed for introspection class Bridge(object): def __init__(self): dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) log.info("Init DBus...") try: self.session_bus = dbus.SessionBus() except dbus.DBusException as e: if e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': log.error(_(u"D-Bus is not launched, please see README to see instructions on how to launch it")) raise BridgeInitError self.dbus_name = dbus.service.BusName(const_INT_PREFIX, self.session_bus) self.dbus_bridge = DbusObject(self.session_bus, const_OBJ_PATH) ##SIGNAL_DIRECT_CALLS_PART## def register_method(self, name, callback): log.debug("registering DBus bridge method [%s]" % name) self.dbus_bridge.register_method(name, callback) def addMethod(self, name, int_suffix, in_sign, out_sign, method, async=False, doc={}): """Dynamically add a method to Dbus Bridge""" #FIXME: doc parameter is kept only temporary, the time to remove it from calls log.debug("Adding method [%s] to DBus bridge" % name) self.dbus_bridge.addMethod(name, int_suffix, in_sign, out_sign, method, async) self.register_method(name, method) def addSignal(self, name, int_suffix, signature, doc={}): self.dbus_bridge.addSignal(name, int_suffix, signature, doc) setattr(Bridge, name, getattr(self.dbus_bridge, name)) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus/dbus_frontend_template.py0000644000175500017600000001324113243470024032525 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SAT communication bridge # Copyright (C) 2009-2018 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 sat.core.i18n import _ from bridge_frontend import BridgeException import dbus from sat.core.log import getLogger log = getLogger(__name__) from sat.core.exceptions import BridgeExceptionNoService, BridgeInitError from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) import ast const_INT_PREFIX = "org.goffi.SAT" # Interface prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = '/org/goffi/SAT/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(object): def bridgeConnect(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, 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(_(u"D-Bus is not launched, please see README to see instructions on how to launch it")) errback(BridgeInitError) else: errback(e) 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 getPluginMethod(*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(unicode(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.warnings(u"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)) return method(*args, **kwargs) return getPluginMethod ##METHODS_PART## sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus/__init__.py0000644000175500017600000000000013243470024027522 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus/constructor.py0000644000175500017600000001111013243470024030354 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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 = { 'signals': """\ @dbus.service.signal(const_INT_PREFIX+const_{category}_SUFFIX, signature='{sig_in}') def {name}(self, {args}): {body}\n""", 'methods': """\ @dbus.service.method(const_INT_PREFIX+const_{category}_SUFFIX, in_signature='{sig_in}', out_signature='{sig_out}', async_callbacks={async_callbacks}) def {name}(self, {args}{async_comma}{async_args_def}): {debug}return self._callback("{name}", {args_result}{async_comma}{async_args_call})\n""", 'signal_direct_calls': """\ def {name}(self, {args}): self.dbus_bridge.{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""", } 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 'log.debug ("%s")\n%s' % (completion['name'], 8 * ' '), 'args_result': self.getArguments(function['sig_in'], name=arg_doc, unicode_protect=self.args.unicode), 'async_comma': ', ' if async_ and function['sig_in'] else '', 'async_args_def': 'callback=None, errback=None' if async_ else '', 'async_args_call': 'callback=callback, errback=errback' if async_ else '', 'async_callbacks': "('callback', 'errback')" if async_ else "None", 'category': completion['category'].upper(), }) 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.getArguments(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'] = ("unicode(%s)" if self.args.unicode and function['sig_out'] == 's' else "%s") % result sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/0002755000175500017600000000000013243470024026221 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/embedded_template.py0000644000175500017600000000751613243470024032226 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions class _Bridge(object): def __init__(self): log.debug(u"Init embedded bridge...") self._methods_cbs = {} self._signals_cbs = { "core": {}, "plugin": {} } def bridgeConnect(self, callback, errback): callback() def register_method(self, name, callback): log.debug(u"registering embedded bridge method [{}]".format(name)) if name in self._methods_cbs: raise exceptions.ConflictError(u"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(u"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 dummy: 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(u"ignoring signal {}: no callback registered".format(name)) else: cb(*args, **kwargs) def addMethod(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 addSignal(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 ././@LongLink0000644000000000000000000000015100000000000011600 Lustar rootrootsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/embedded_frontend_template.pysat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/embedded_frontend_templat0000644000175500017600000000147013243470024033322 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.bridge.embedded import Bridge sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/__init__.py0000644000175500017600000000000013243470024030316 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/embedded/constructor.py0000644000175500017600000000611413243470024031160 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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.getArguments(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 dummy: 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.getArguments(function['sig_in'], name=arg_doc), }) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/mediawiki/0002755000175500017600000000000013243470024026433 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/mediawiki/__init__.py0000644000175500017600000000000013243470024030530 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/mediawiki/constructor.py0000644000175500017600000001335613243470024031400 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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 _addTextDecorations(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 _wikiParameter(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.getArgumentsDoc(name) arg_default = self.getDefault(name) args_str = self.getArguments(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._addTextDecorations(doc))) else: wiki.append("; arg_%d: " % i) if i in arg_default: wiki.append(":''DEFAULT: %s''" % arg_default[i]) return "\n".join(wiki) def _wikiReturn(self, name): """Format return doc with the wiki syntax @param name: name of the function """ arg_doc = self.getArgumentsDoc(name) wiki = [] if 'return' in arg_doc: wiki.append('\n|-\n! scope=row | return value\n|') wiki.append('
\n'.join(self._addTextDecorations(arg_doc['return']).rstrip('\n').split('\n'))) return "\n".join(wiki) def generateCoreSide(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.getDoc(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._wikiParameter(section, function['sig_in']), 'return': self._wikiReturn(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.getTemplatePath(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.finalWrite(self.core_dest, core_bridge) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/mediawiki/mediawiki_template.tpl0000644000175500017600000000032413243470024033007 0ustar debaclelocal_src[[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## sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus-xml/0002755000175500017600000000000013243470024026223 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus-xml/__init__.py0000644000175500017600000000000013243470024030320 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus-xml/constructor.py0000644000175500017600000001005113243470024031155 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.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.goffi.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 generateCoreSide(self): try: doc = minidom.parse(self.getTemplatePath(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.getArgumentsDoc(section) for arg in self.argumentsParser(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.finalWrite(self.core_dest, [doc.toprettyxml()]) sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/dbus-xml/dbus_xml_template.xml0000644000175500017600000000010613243470024032450 0ustar debaclelocal_src sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constructors/__init__.py0000644000175500017600000000000013243470024026565 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/constants.py0000644000175500017600000000304413243470024024305 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.core import constants class Const(constants.Const): NAME = u"bridge_constructor" DEST_DIR_DEFAULT = "generated" DESCRIPTION = u"""{name} Copyright (C) 2009-2018 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 sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/bridge_template.ini0000644000175500017600000005230113243470024025547 0ustar debaclelocal_src[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=%(doc_profile)s doc_param_1=jid_s: the JID that we were assigned by the server, as the resource might differ from the JID we asked for. [disconnected] type=signal category=core sig_in=s doc=Connection is finished or lost doc_param_0=%(doc_profile)s [newContact] 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 [messageNew] type=signal category=core sig_in=sdssa{ss}a{ss}sa{ss}s 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 [presenceUpdate] 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 [paramUpdate] 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 [contactDeleted] 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 [actionNew] type=signal category=core sig_in=a{ss}sis doc=A frontend action is requested doc_param_0=action_data: a dict where key can be: - xmlui: a XMLUI need to be displayed - 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 [entityDataUpdated] 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 [progressStarted] 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 [progressFinished] 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 ([progressError] 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 [progressError] 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 ;methods [getReady] async= type=method category=core sig_in= sig_out= doc=Return when backend is initialised [getVersion] type=method category=core sig_in= sig_out=s doc=Get "Salut à Toi" full version [getFeatures] 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= [profileNameGet] 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 [profilesListGet] 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 [profileSetDefault] type=method category=core sig_in=s sig_out= doc_param_0=%(doc_profile)s doc=Set default profile [getEntityData] 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 [getEntitiesData] 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 if key doesn't exist for a jid, the resulting dictionary will not have it [profileCreate] 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 [asyncDeleteProfile] 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 [profileStartSession] 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 [profileIsSessionStarted] 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 [isConnected] 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 [getContacts] 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 [newContact] - groups where the contact is [getContactsFromGroup] 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 [getMainResource] 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 "" [getPresenceStatuses] 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 [presenceUpdate] [getWaitingSub] 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 [messageSend] async= type=method category=core sig_in=sa{ss}a{ss}sa{ss}s 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: optional data that can be used by a plugin to build more specific messages doc_param_5=%(doc_profile_key)s [setPresence] 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 [presenceUpdate] doc_param_2=statuses: as for [presenceUpdate] 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 [getConfig] 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 [setParam] 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 [getParamA] 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 [setParam] doc_param_1=category: as for [setParam] doc_param_2=attribute: Name of the attribute doc_param_3=%(doc_profile_key)s [asyncGetParamA] 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 [setParam] doc_param_1=category: as for [setParam] doc_param_2=attribute: Name of the attribute doc_param_3=%(doc_security_limit)s doc_param_4=%(doc_profile_key)s [asyncGetParamsValuesFromCategory] async= type=method category=code sig_in=sis sig_out=a{ss} param_1_default=-1 param_2_default="@DEFAULT@" doc=Get "attribute" for all params of a category doc_param_0=category: as for [setParam] doc_param_1=%(doc_security_limit)s doc_param_2=%(doc_profile_key)s [getParamsUI] async= type=method category=core sig_in=iss sig_out=s param_0_default=-1 param_1_default='' param_2_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=%(doc_profile_key)s [getParamsCategories] type=method category=core sig_in= sig_out=as doc=Get all categories currently existing in parameters doc_return=list of categories [paramsRegisterApp] 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 [historyGet] async= type=method category=core sig_in=ssiba{ss}s sig_out=a(sdssa{ss}a{ss}sa{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 one of those, values are separated by spaces - not_types: type must not be one of those, values are separated by spaces doc_param_5=%(doc_profile)s doc_return=Ordered list (by timestamp) of data as in [messageNew] (without final profile) [addContact] 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 [updateContact] async= 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 [delContact] 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 [launchAction] async= type=method category=core sig_in=sa{ss}s sig_out=a{ss} 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 [actionsGet] type=method category=core sig_in=s sig_out=a(a{ss}si) 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 [actionNew] (without the profile) [progressGet] 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 [progressGetAllMetadata] 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 [progressStarted] [progressGetAll] 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 [progressGet] [menusGet] 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 [menuLaunch] 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 [menuHelpGet] 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 [discoInfos] 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=u"@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 [discoItems] async= type=method category=core sig_in=ssbs sig_out=a(sss) param_1_default=u'' param_2_default=True param_3_default=u"@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) [saveParamsTemplate] 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) [loadParamsTemplate] 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) [sessionInfosGet] 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) [namespacesGet] type=method category=core sig_in= sig_out=a{ss} doc=Get a dict to short name => whole namespaces doc_return=namespaces mapping sat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/__init__.py0000644000175500017600000000000013243470024024015 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/bridge_constructor/bridge_constructor.py0000755000175500017600000001070513243470024026177 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SàT: a XMPP client # Copyright (C) 2009-2018 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 sat.bridge import bridge_constructor from sat.bridge.bridge_constructor.constants import Const as C from sat.bridge.bridge_constructor import constructors, base_constructor import argparse from ConfigParser import SafeConfigParser as Parser from importlib import import_module import os import os.path #consts __version__ = C.APP_VERSION class BridgeConstructor(object): def importConstructors(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 = "sat.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=file, 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.importConstructors() args = self.parse_args() template_parser = Parser() try: template_parser.readfp(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() sat-0.6.1.1+hg20180208/src/bridge/__init__.py0000644000175500017600000000000013243470024020114 0ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/bridge/dbus_bridge.py0000755000175500017600000007602113243470024020651 0ustar debaclelocal_src#!/usr/bin/env python2 #-*- coding: utf-8 -*- # SAT: a jabber client # Copyright (C) 2009-2018 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 sat.core.i18n import _ import dbus import dbus.service import dbus.mainloop.glib import inspect from sat.core.log import getLogger log = getLogger(__name__) from twisted.internet.defer import Deferred from sat.core.exceptions import BridgeInitError const_INT_PREFIX = "org.goffi.SAT" # Interface prefix const_ERROR_PREFIX = const_INT_PREFIX + ".error" const_OBJ_PATH = '/org/goffi/SAT/bridge' const_CORE_SUFFIX = ".core" const_PLUGIN_SUFFIX = ".plugin" class ParseError(Exception): pass class MethodNotRegistered(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".MethodNotRegistered" class InternalError(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".InternalError" class AsyncNotDeferred(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".AsyncNotDeferred" class DeferredNotAsync(dbus.DBusException): _dbus_error_name = const_ERROR_PREFIX + ".DeferredNotAsync" class GenericException(dbus.DBusException): def __init__(self, twisted_error): """ @param twisted_error (Failure): instance of twisted Failure @return: DBusException """ super(GenericException, self).__init__() 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__ message = twisted_error.getErrorMessage() try: self.args = (message, twisted_error.value.condition) except AttributeError: self.args = (message,) self._dbus_error_name = '.'.join([const_ERROR_PREFIX, class_.__module__, class_.__name__]) class DbusObject(dbus.service.Object): def __init__(self, bus, path): dbus.service.Object.__init__(self, bus, 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 if the callback return a deferred, use async methods""" if not name in self.cb: raise MethodNotRegistered if "callback" in kwargs: #we must have errback too if not "errback" in kwargs: log.error("errback is missing in method call [%s]" % name) raise InternalError callback = kwargs.pop("callback") errback = kwargs.pop("errback") async = True else: async = False result = self.cb[name](*args, **kwargs) if async: if not isinstance(result, Deferred): log.error("Asynchronous method [%s] does not return a Deferred." % name) raise AsyncNotDeferred result.addCallback(lambda result: callback() if result is None else callback(result)) result.addErrback(lambda err: errback(GenericException(err))) else: if isinstance(result, Deferred): log.error("Synchronous method [%s] return a Deferred." % name) raise DeferredNotAsync return result ### signals ### @dbus.service.signal(const_INT_PREFIX + const_PLUGIN_SUFFIX, signature='') def dummySignal(self): #FIXME: workaround for addSignal (doesn't work if one method doensn't # already exist for plugins), probably missing some initialisation, need # further investigations pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='a{ss}sis') def actionNew(self, action_data, id, security_limit, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ss') def connected(self, profile, jid_s): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ss') def contactDeleted(self, entity_jid, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='s') def disconnected(self, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ssss') def entityDataUpdated(self, jid, name, value, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sdssa{ss}a{ss}sa{ss}s') def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sa{ss}ass') def newContact(self, contact_jid, attributes, groups, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ssss') def paramUpdate(self, name, value, category, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='ssia{ss}s') def presenceUpdate(self, entity_jid, show, priority, statuses, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sss') def progressError(self, id, error, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sa{ss}s') def progressFinished(self, id, metadata, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sa{ss}s') def progressStarted(self, id, metadata, profile): pass @dbus.service.signal(const_INT_PREFIX+const_CORE_SUFFIX, signature='sss') def subscribe(self, sub_type, entity_jid, profile): pass ### methods ### @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a(a{ss}si)', async_callbacks=None) def actionsGet(self, profile_key="@DEFAULT@"): return self._callback("actionsGet", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='', async_callbacks=None) def addContact(self, entity_jid, profile_key="@DEFAULT@"): return self._callback("addContact", unicode(entity_jid), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='', async_callbacks=('callback', 'errback')) def asyncDeleteProfile(self, profile, callback=None, errback=None): return self._callback("asyncDeleteProfile", unicode(profile), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sssis', out_signature='s', async_callbacks=('callback', 'errback')) def asyncGetParamA(self, name, category, attribute="value", security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("asyncGetParamA", unicode(name), unicode(category), unicode(attribute), security_limit, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sis', out_signature='a{ss}', async_callbacks=('callback', 'errback')) def asyncGetParamsValuesFromCategory(self, category, security_limit=-1, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("asyncGetParamsValuesFromCategory", unicode(category), security_limit, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssa{ss}', out_signature='b', async_callbacks=('callback', 'errback')) def connect(self, profile_key="@DEFAULT@", password='', options={}, callback=None, errback=None): return self._callback("connect", unicode(profile_key), unicode(password), options, callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='', async_callbacks=('callback', 'errback')) def delContact(self, entity_jid, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("delContact", unicode(entity_jid), unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssbs', out_signature='(asa(sss)a{sa(a{ss}as)})', async_callbacks=('callback', 'errback')) def discoInfos(self, entity_jid, node=u'', use_cache=True, profile_key=u"@DEFAULT@", callback=None, errback=None): return self._callback("discoInfos", unicode(entity_jid), unicode(node), use_cache, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssbs', out_signature='a(sss)', async_callbacks=('callback', 'errback')) def discoItems(self, entity_jid, node=u'', use_cache=True, profile_key=u"@DEFAULT@", callback=None, errback=None): return self._callback("discoItems", unicode(entity_jid), unicode(node), use_cache, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='', async_callbacks=('callback', 'errback')) def disconnect(self, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("disconnect", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) def getConfig(self, section, name): return self._callback("getConfig", unicode(section), unicode(name)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a(sa{ss}as)', async_callbacks=('callback', 'errback')) def getContacts(self, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("getContacts", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='as', async_callbacks=None) def getContactsFromGroup(self, group, profile_key="@DEFAULT@"): return self._callback("getContactsFromGroup", unicode(group), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='asass', out_signature='a{sa{ss}}', async_callbacks=None) def getEntitiesData(self, jids, keys, profile): return self._callback("getEntitiesData", jids, keys, unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sass', out_signature='a{ss}', async_callbacks=None) def getEntityData(self, jid, keys, profile): return self._callback("getEntityData", unicode(jid), keys, unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{sa{ss}}', async_callbacks=('callback', 'errback')) def getFeatures(self, profile_key, callback=None, errback=None): return self._callback("getFeatures", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) def getMainResource(self, contact_jid, profile_key="@DEFAULT@"): return self._callback("getMainResource", unicode(contact_jid), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssss', out_signature='s', async_callbacks=None) def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@"): return self._callback("getParamA", unicode(name), unicode(category), unicode(attribute), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='', out_signature='as', async_callbacks=None) def getParamsCategories(self, ): return self._callback("getParamsCategories", ) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='iss', out_signature='s', async_callbacks=('callback', 'errback')) def getParamsUI(self, security_limit=-1, app='', profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("getParamsUI", security_limit, unicode(app), unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{sa{s(sia{ss})}}', async_callbacks=None) def getPresenceStatuses(self, profile_key="@DEFAULT@"): return self._callback("getPresenceStatuses", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='', out_signature='', async_callbacks=('callback', 'errback')) def getReady(self, callback=None, errback=None): return self._callback("getReady", callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='', out_signature='s', async_callbacks=None) def getVersion(self, ): return self._callback("getVersion", ) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{ss}', async_callbacks=None) def getWaitingSub(self, profile_key="@DEFAULT@"): return self._callback("getWaitingSub", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssiba{ss}s', out_signature='a(sdssa{ss}a{ss}sa{ss})', async_callbacks=('callback', 'errback')) def historyGet(self, from_jid, to_jid, limit, between=True, filters='', profile="@NONE@", callback=None, errback=None): return self._callback("historyGet", unicode(from_jid), unicode(to_jid), limit, between, filters, unicode(profile), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def isConnected(self, profile_key="@DEFAULT@"): return self._callback("isConnected", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sa{ss}s', out_signature='a{ss}', async_callbacks=('callback', 'errback')) def launchAction(self, callback_id, data, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("launchAction", unicode(callback_id), data, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def loadParamsTemplate(self, filename): return self._callback("loadParamsTemplate", unicode(filename)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='s', async_callbacks=None) def menuHelpGet(self, menu_id, language): return self._callback("menuHelpGet", unicode(menu_id), unicode(language)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sasa{ss}is', out_signature='a{ss}', async_callbacks=('callback', 'errback')) def menuLaunch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None): return self._callback("menuLaunch", unicode(menu_type), path, data, security_limit, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='si', out_signature='a(ssasasa{ss})', async_callbacks=None) def menusGet(self, language, security_limit): return self._callback("menusGet", unicode(language), security_limit) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sa{ss}a{ss}sa{ss}s', out_signature='', async_callbacks=('callback', 'errback')) def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None): return self._callback("messageSend", unicode(to_jid), message, subject, unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='', out_signature='a{ss}', async_callbacks=None) def namespacesGet(self, ): return self._callback("namespacesGet", ) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sis', out_signature='', async_callbacks=None) def paramsRegisterApp(self, xml, security_limit=-1, app=''): return self._callback("paramsRegisterApp", unicode(xml), security_limit, unicode(app)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sss', out_signature='', async_callbacks=('callback', 'errback')) def profileCreate(self, profile, password='', component='', callback=None, errback=None): return self._callback("profileCreate", unicode(profile), unicode(password), unicode(component), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def profileIsSessionStarted(self, profile_key="@DEFAULT@"): return self._callback("profileIsSessionStarted", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='s', async_callbacks=None) def profileNameGet(self, profile_key="@DEFAULT@"): return self._callback("profileNameGet", unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='', async_callbacks=None) def profileSetDefault(self, profile): return self._callback("profileSetDefault", unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='b', async_callbacks=('callback', 'errback')) def profileStartSession(self, password='', profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("profileStartSession", unicode(password), unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='bb', out_signature='as', async_callbacks=None) def profilesListGet(self, clients=True, components=False): return self._callback("profilesListGet", clients, components) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ss', out_signature='a{ss}', async_callbacks=None) def progressGet(self, id, profile): return self._callback("progressGet", unicode(id), unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{sa{sa{ss}}}', async_callbacks=None) def progressGetAll(self, profile): return self._callback("progressGetAll", unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{sa{sa{ss}}}', async_callbacks=None) def progressGetAllMetadata(self, profile): return self._callback("progressGetAllMetadata", unicode(profile)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='b', async_callbacks=None) def saveParamsTemplate(self, filename): return self._callback("saveParamsTemplate", unicode(filename)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='a{ss}', async_callbacks=('callback', 'errback')) def sessionInfosGet(self, profile_key, callback=None, errback=None): return self._callback("sessionInfosGet", unicode(profile_key), callback=callback, errback=errback) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sssis', out_signature='', async_callbacks=None) def setParam(self, name, value, category, security_limit=-1, profile_key="@DEFAULT@"): return self._callback("setParam", unicode(name), unicode(value), unicode(category), security_limit, unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssa{ss}s', out_signature='', async_callbacks=None) def setPresence(self, to_jid='', show='', statuses={}, profile_key="@DEFAULT@"): return self._callback("setPresence", unicode(to_jid), unicode(show), statuses, unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='sss', out_signature='', async_callbacks=None) def subscription(self, sub_type, entity, profile_key="@DEFAULT@"): return self._callback("subscription", unicode(sub_type), unicode(entity), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssass', out_signature='', async_callbacks=('callback', 'errback')) def updateContact(self, entity_jid, name, groups, profile_key="@DEFAULT@", callback=None, errback=None): return self._callback("updateContact", unicode(entity_jid), unicode(name), groups, unicode(profile_key), callback=callback, errback=errback) def __attributes(self, in_sign): """Return arguments to user given a in_sign @param in_sign: in_sign in the short form (using s,a,i,b etc) @return: list of arguments that correspond to a in_sign (e.g.: "sss" return "arg1, arg2, arg3")""" i = 0 idx = 0 attr = [] while i < len(in_sign): if in_sign[i] not in ['b', 'y', 'n', 'i', 'x', 'q', 'u', 't', 'd', 's', 'a']: raise ParseError("Unmanaged attribute type [%c]" % in_sign[i]) attr.append("arg_%i" % idx) idx += 1 if in_sign[i] == 'a': i += 1 if in_sign[i] != '{' and in_sign[i] != '(': # FIXME: must manage tuples out of arrays i += 1 continue # we have a simple type for the array opening_car = in_sign[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(in_sign): raise ParseError("missing }") if in_sign[i] == opening_car: opening_count += 1 if in_sign[i] == closing_car: opening_count -= 1 if opening_count == 0: break i += 1 return attr def addMethod(self, name, int_suffix, in_sign, out_sign, method, async=False): """Dynamically add a method to Dbus Bridge""" inspect_args = inspect.getargspec(method) _arguments = inspect_args.args _defaults = list(inspect_args.defaults or []) if inspect.ismethod(method): #if we have a method, we don't want the first argument (usually 'self') del(_arguments[0]) #first arguments are for the _callback method arguments_callback = ', '.join([repr(name)] + ((_arguments + ['callback=callback', 'errback=errback']) if async else _arguments)) if async: _arguments.extend(['callback', 'errback']) _defaults.extend([None, None]) #now we create a second list with default values for i in range(1, len(_defaults) + 1): _arguments[-i] = "%s = %s" % (_arguments[-i], repr(_defaults[-i])) arguments_defaults = ', '.join(_arguments) code = compile('def %(name)s (self,%(arguments_defaults)s): return self._callback(%(arguments_callback)s)' % {'name': name, 'arguments_defaults': arguments_defaults, 'arguments_callback': arguments_callback}, '', 'exec') exec (code) # FIXME: to the same thing in a cleaner way, without compile/exec method = locals()[name] async_callbacks = ('callback', 'errback') if async else None setattr(DbusObject, name, dbus.service.method( const_INT_PREFIX + int_suffix, in_signature=in_sign, out_signature=out_sign, async_callbacks=async_callbacks)(method)) function = getattr(self, name) func_table = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__][function._dbus_interface] func_table[function.__name__] = function # Needed for introspection def addSignal(self, name, int_suffix, signature, doc={}): """Dynamically add a signal to Dbus Bridge""" attributes = ', '.join(self.__attributes(signature)) #TODO: use doc parameter to name attributes #code = compile ('def '+name+' (self,'+attributes+'): log.debug ("'+name+' signal")', '','exec') #XXX: the log.debug is too annoying with xmllog code = compile('def ' + name + ' (self,' + attributes + '): pass', '', 'exec') exec (code) signal = locals()[name] setattr(DbusObject, name, dbus.service.signal( const_INT_PREFIX + int_suffix, signature=signature)(signal)) function = getattr(self, name) func_table = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__][function._dbus_interface] func_table[function.__name__] = function # Needed for introspection class Bridge(object): def __init__(self): dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) log.info("Init DBus...") try: self.session_bus = dbus.SessionBus() except dbus.DBusException as e: if e._dbus_error_name == 'org.freedesktop.DBus.Error.NotSupported': log.error(_(u"D-Bus is not launched, please see README to see instructions on how to launch it")) raise BridgeInitError self.dbus_name = dbus.service.BusName(const_INT_PREFIX, self.session_bus) self.dbus_bridge = DbusObject(self.session_bus, const_OBJ_PATH) def actionNew(self, action_data, id, security_limit, profile): self.dbus_bridge.actionNew(action_data, id, security_limit, profile) def connected(self, profile, jid_s): self.dbus_bridge.connected(profile, jid_s) def contactDeleted(self, entity_jid, profile): self.dbus_bridge.contactDeleted(entity_jid, profile) def disconnected(self, profile): self.dbus_bridge.disconnected(profile) def entityDataUpdated(self, jid, name, value, profile): self.dbus_bridge.entityDataUpdated(jid, name, value, profile) def messageNew(self, uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile): self.dbus_bridge.messageNew(uid, timestamp, from_jid, to_jid, message, subject, mess_type, extra, profile) def newContact(self, contact_jid, attributes, groups, profile): self.dbus_bridge.newContact(contact_jid, attributes, groups, profile) def paramUpdate(self, name, value, category, profile): self.dbus_bridge.paramUpdate(name, value, category, profile) def presenceUpdate(self, entity_jid, show, priority, statuses, profile): self.dbus_bridge.presenceUpdate(entity_jid, show, priority, statuses, profile) def progressError(self, id, error, profile): self.dbus_bridge.progressError(id, error, profile) def progressFinished(self, id, metadata, profile): self.dbus_bridge.progressFinished(id, metadata, profile) def progressStarted(self, id, metadata, profile): self.dbus_bridge.progressStarted(id, metadata, profile) def subscribe(self, sub_type, entity_jid, profile): self.dbus_bridge.subscribe(sub_type, entity_jid, profile) def register_method(self, name, callback): log.debug("registering DBus bridge method [%s]" % name) self.dbus_bridge.register_method(name, callback) def addMethod(self, name, int_suffix, in_sign, out_sign, method, async=False, doc={}): """Dynamically add a method to Dbus Bridge""" #FIXME: doc parameter is kept only temporary, the time to remove it from calls log.debug("Adding method [%s] to DBus bridge" % name) self.dbus_bridge.addMethod(name, int_suffix, in_sign, out_sign, method, async) self.register_method(name, method) def addSignal(self, name, int_suffix, signature, doc={}): self.dbus_bridge.addSignal(name, int_suffix, signature, doc) setattr(Bridge, name, getattr(self.dbus_bridge, name))sat-0.6.1.1+hg20180208/src/plugins/0002755000175500017600000000000013243470025016245 5ustar debaclelocal_srcsat-0.6.1.1+hg20180208/src/plugins/plugin_misc_android.py0000644000175500017600000000713113243470024022627 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for file tansfer # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions import sys import mmap PLUGIN_INFO = { C.PI_NAME: "Android ", C.PI_IMPORT_NAME: "android", C.PI_TYPE: C.PLUG_TYPE_MISC, C.PI_MAIN: "AndroidPlugin", C.PI_HANDLER: "no", C.PI_DESCRIPTION: D_("""Manage Android platform specificities, like pause or notifications""") } if sys.platform != "android": raise exceptions.CancelError(u"this module is not needed on this platform") from plyer import notification, vibrator PARAM_VIBRATE_CATEGORY = "Notifications" PARAM_VIBRATE_NAME = "vibrate" PARAM_VIBRATE_LABEL = D_(u"Vibrate on notifications") class AndroidPlugin(object): params = """ """.format( category_name = PARAM_VIBRATE_CATEGORY, category_label = D_(PARAM_VIBRATE_CATEGORY), param_name = PARAM_VIBRATE_NAME, param_label = PARAM_VIBRATE_LABEL, ) def __init__(self, host): log.info(_("plugin Android initialization")) self.host = host host.memory.updateParams(self.params) self.cagou_status_fd = open('.cagou_status', 'rb') self.cagou_status = mmap.mmap(self.cagou_status_fd.fileno(), 1, prot=mmap.PROT_READ) # we set a low priority because we want the notification to be sent after all plugins have done their job host.trigger.add("MessageReceived", self.messageReceivedTrigger, priority=-1000) @property def cagou_active(self): # 'R' status means Cagou is running in front return self.cagou_status[0] == 'R' def _notifyMessage(self, mess_data, client): # send notification if there is a message and it is not a groupchat if mess_data['message'] and mess_data['type'] != C.MESS_TYPE_GROUPCHAT: message = mess_data['message'].itervalues().next() try: subject = mess_data['subject'].itervalues().next() except StopIteration: subject = u'Cagou new message' notification.notify( title = subject, message = message ) if self.host.memory.getParamA(PARAM_VIBRATE_NAME, PARAM_VIBRATE_CATEGORY, profile_key=client.profile): vibrator.vibrate() return mess_data def messageReceivedTrigger(self, client, message_elt, post_treat): if not self.cagou_active: # we only send notification is the frontend is not displayed post_treat.addCallback(self._notifyMessage, client) return True sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_tarot.py0000755000175500017600000010426613243470025022353 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing French Tarot game # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from twisted.internet import defer from wokkel import data_form from sat.memory import memory from sat.tools import xml_tools from sat_frontends.tools.games import TarotCard import random NS_CG = 'http://www.goffi.org/protocol/card_game' CG_TAG = 'card_game' PLUGIN_INFO = { C.PI_NAME: "Tarot cards plugin", C.PI_IMPORT_NAME: "Tarot", C.PI_TYPE: "Misc", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249", "ROOM-GAME"], C.PI_MAIN: "Tarot", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Tarot card game""") } class Tarot(object): def inheritFromRoomGame(self, host): global RoomGame RoomGame = host.plugins["ROOM-GAME"].__class__ self.__class__ = type(self.__class__.__name__, (self.__class__, RoomGame, object), {}) def __init__(self, host): log.info(_("Plugin Tarot initialization")) self._sessions = memory.Sessions() self.inheritFromRoomGame(host) RoomGame._init_(self, host, PLUGIN_INFO, (NS_CG, CG_TAG), game_init={'hand_size': 18, 'init_player': 0, 'current_player': None, 'contrat': None, 'stage': None}, player_init={'score': 0}) self.contrats = [_('Passe'), _('Petite'), _('Garde'), _('Garde Sans'), _('Garde Contre')] host.bridge.addMethod("tarotGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self._prepareRoom, async=True) # args: players, room_jid, profile host.bridge.addMethod("tarotGameCreate", ".plugin", in_sign='sass', out_sign='', method=self._createGame) # args: room_jid, players, profile host.bridge.addMethod("tarotGameReady", ".plugin", in_sign='sss', out_sign='', method=self._playerReady) # args: player, referee, profile host.bridge.addMethod("tarotGamePlayCards", ".plugin", in_sign='ssa(ss)s', out_sign='', method=self.play_cards) # args: player, referee, cards, profile host.bridge.addSignal("tarotGamePlayers", ".plugin", signature='ssass') # args: room_jid, referee, players, profile host.bridge.addSignal("tarotGameStarted", ".plugin", signature='ssass') # args: room_jid, referee, players, profile host.bridge.addSignal("tarotGameNew", ".plugin", signature='sa(ss)s') # args: room_jid, hand, profile host.bridge.addSignal("tarotGameChooseContrat", ".plugin", signature='sss') # args: room_jid, xml_data, profile host.bridge.addSignal("tarotGameShowCards", ".plugin", signature='ssa(ss)a{ss}s') # args: room_jid, type ["chien", "poignée",...], cards, data[dict], profile host.bridge.addSignal("tarotGameCardsPlayed", ".plugin", signature='ssa(ss)s') # args: room_jid, player, type ["chien", "poignée",...], cards, data[dict], profile host.bridge.addSignal("tarotGameYourTurn", ".plugin", signature='ss') # args: room_jid, profile host.bridge.addSignal("tarotGameScore", ".plugin", signature='ssasass') # args: room_jid, xml_data, winners (list of nicks), loosers (list of nicks), profile host.bridge.addSignal("tarotGameInvalidCards", ".plugin", signature='ssa(ss)a(ss)s') # args: room_jid, game phase, played_cards, invalid_cards, profile self.deck_ordered = [] for value in ['excuse'] + map(str, range(1, 22)): self.deck_ordered.append(TarotCard(("atout", value))) for suit in ["pique", "coeur", "carreau", "trefle"]: for value in map(str, range(1, 11)) + ["valet", "cavalier", "dame", "roi"]: self.deck_ordered.append(TarotCard((suit, value))) self.__choose_contrat_id = host.registerCallback(self._contratChoosed, with_data=True) self.__score_id = host.registerCallback(self._scoreShowed, with_data=True) def __card_list_to_xml(self, cards_list, elt_name): """Convert a card list to domish element""" cards_list_elt = domish.Element((None, elt_name)) for card in cards_list: card_elt = domish.Element((None, 'card')) card_elt['suit'] = card.suit card_elt['value'] = card.value cards_list_elt.addChild(card_elt) return cards_list_elt def __xml_to_list(self, cards_list_elt): """Convert a domish element with cards to a list of tuples""" cards_list = [] for card in cards_list_elt.elements(): cards_list.append((card['suit'], card['value'])) return cards_list def __ask_contrat(self): """Create a element for asking contrat""" contrat_elt = domish.Element((None, 'contrat')) form = data_form.Form('form', title=_('contrat selection')) field = data_form.Field('list-single', 'contrat', options=map(data_form.Option, self.contrats), required=True) form.addField(field) contrat_elt.addChild(form.toElement()) return contrat_elt def __give_scores(self, scores, winners, loosers): """Create an element to give scores @param scores: unicode (can contain line feed) @param winners: list of unicode nicks of winners @param loosers: list of unicode nicks of loosers""" score_elt = domish.Element((None, 'score')) form = data_form.Form('form', title=_('scores')) for line in scores.split('\n'): field = data_form.Field('fixed', value=line) form.addField(field) score_elt.addChild(form.toElement()) for winner in winners: winner_elt = domish.Element((None, 'winner')) winner_elt.addContent(winner) score_elt.addChild(winner_elt) for looser in loosers: looser_elt = domish.Element((None, 'looser')) looser_elt.addContent(looser) score_elt.addChild(looser_elt) return score_elt def __invalid_cards_elt(self, played_cards, invalid_cards, game_phase): """Create a element for invalid_cards error @param list_cards: list of Card @param game_phase: phase of the game ['ecart', 'play']""" error_elt = domish.Element((None, 'error')) played_elt = self.__card_list_to_xml(played_cards, 'played') invalid_elt = self.__card_list_to_xml(invalid_cards, 'invalid') error_elt['type'] = 'invalid_cards' error_elt['phase'] = game_phase error_elt.addChild(played_elt) error_elt.addChild(invalid_elt) return error_elt def __next_player(self, game_data, next_pl=None): """Increment player number & return player name @param next_pl: if given, then next_player is forced to this one """ if next_pl: game_data['current_player'] = game_data['players'].index(next_pl) return next_pl else: pl_idx = game_data['current_player'] = (game_data['current_player'] + 1) % len(game_data['players']) return game_data['players'][pl_idx] def __winner(self, game_data): """give the nick of the player who win this trick""" players_data = game_data['players_data'] first = game_data['first_player'] first_idx = game_data['players'].index(first) suit_asked = None strongest = None winner = None for idx in [(first_idx + i) % 4 for i in range(4)]: player = game_data['players'][idx] card = players_data[player]['played'] if card.value == "excuse": continue if suit_asked is None: suit_asked = card.suit if (card.suit == suit_asked or card.suit == "atout") and card > strongest: strongest = card winner = player assert winner return winner def __excuse_hack(self, game_data, played, winner): """give a low card to other team and keep excuse if trick is lost @param game_data: data of the game @param played: cards currently on the table @param winner: nick of the trick winner""" # TODO: manage the case where excuse is played on the last trick (and lost) players_data = game_data['players_data'] excuse = TarotCard(("atout", "excuse")) # we first check if the Excuse was already played # and if somebody is waiting for a card for player in game_data['players']: if players_data[player]['wait_for_low']: # the excuse owner has to give a card to somebody if winner == player: # the excuse owner win the trick, we check if we have something to give for card in played: if card.points == 0.5: pl_waiting = players_data[player]['wait_for_low'] played.remove(card) players_data[pl_waiting]['levees'].append(card) log.debug(_(u'Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensation') % {"excuse_owner": player, "card_waited": card, "player_waiting": pl_waiting}) return return if excuse not in played: # the Excuse is not on the table, nothing to do return excuse_player = None # Who has played the Excuse ? for player in game_data['players']: if players_data[player]['played'] == excuse: excuse_player = player break if excuse_player == winner: return # the excuse player win the trick, nothing to do # first we remove the excuse from played cards played.remove(excuse) # then we give it back to the original owner owner_levees = players_data[excuse_player]['levees'] owner_levees.append(excuse) # finally we give a low card to the trick winner low_card = None # We look backward in cards won by the Excuse owner to # find a low value card for card_idx in range(len(owner_levees) - 1, -1, -1): if owner_levees[card_idx].points == 0.5: low_card = owner_levees[card_idx] del owner_levees[card_idx] players_data[winner]['levees'].append(low_card) log.debug(_(u'Player %(excuse_owner)s give %(card_waited)s to %(player_waiting)s for Excuse compensation') % {"excuse_owner": excuse_player, "card_waited": low_card, "player_waiting": winner}) break if not low_card: # The player has no low card yet # TODO: manage case when player never win a trick with low card players_data[excuse_player]['wait_for_low'] = winner log.debug(_(u"%(excuse_owner)s keep the Excuse but has not card to give, %(winner)s is waiting for one") % {'excuse_owner': excuse_player, 'winner': winner}) def __draw_game(self, game_data): """The game is draw, no score change @param game_data: data of the game @return: tuple with (string victory message, list of winners, list of loosers)""" players_data = game_data['players_data'] scores_str = _('Draw game') scores_str += '\n' for player in game_data['players']: scores_str += _(u"\n--\n%(player)s:\nscore for this game ==> %(score_game)i\ntotal score ==> %(total_score)i") % {'player': player, 'score_game': 0, 'total_score': players_data[player]['score']} log.debug(scores_str) return (scores_str, [], []) def __calculate_scores(self, game_data): """The game is finished, time to know who won :) @param game_data: data of the game @return: tuple with (string victory message, list of winners, list of loosers)""" players_data = game_data['players_data'] levees = players_data[game_data['attaquant']]['levees'] score = 0 nb_bouts = 0 bouts = [] for card in levees: if card.bout: nb_bouts += 1 bouts.append(card.value) score += card.points # We do a basic check on score calculation check_score = 0 defenseurs = game_data['players'][:] defenseurs.remove(game_data['attaquant']) for defenseur in defenseurs: for card in players_data[defenseur]['levees']: check_score += card.points if game_data['contrat'] == "Garde Contre": for card in game_data['chien']: check_score += card.points assert (score + check_score == 91) point_limit = None if nb_bouts == 3: point_limit = 36 elif nb_bouts == 2: point_limit = 41 elif nb_bouts == 1: point_limit = 51 else: point_limit = 56 if game_data['contrat'] == 'Petite': contrat_mult = 1 elif game_data['contrat'] == 'Garde': contrat_mult = 2 elif game_data['contrat'] == 'Garde Sans': contrat_mult = 4 elif game_data['contrat'] == 'Garde Contre': contrat_mult = 6 else: log.error(_('INTERNAL ERROR: contrat not managed (mispelled ?)')) assert(False) victory = (score >= point_limit) margin = abs(score - point_limit) points_defenseur = (margin + 25) * contrat_mult * (-1 if victory else 1) winners = [] loosers = [] player_score = {} for player in game_data['players']: # TODO: adjust this for 3 and 5 players variants # TODO: manage bonuses (petit au bout, poignée, chelem) player_score[player] = points_defenseur if player != game_data['attaquant'] else points_defenseur * -3 players_data[player]['score'] += player_score[player] # we add score of this game to the global score if player_score[player] > 0: winners.append(player) else: loosers.append(player) scores_str = _(u'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') % {'attaquant': game_data['attaquant'], 'points': score, 'point_limit': point_limit, 'nb_bouts': nb_bouts, 'plural': 's' if nb_bouts > 1 else '', 'separator': ': ' if nb_bouts != 0 else '', 'bouts': ','.join(map(str, bouts)), 'victory': 'wins' if victory else 'looses'} scores_str += '\n' for player in game_data['players']: scores_str += _(u"\n--\n%(player)s:\nscore for this game ==> %(score_game)i\ntotal score ==> %(total_score)i") % {'player': player, 'score_game': player_score[player], 'total_score': players_data[player]['score']} log.debug(scores_str) return (scores_str, winners, loosers) def __invalid_cards(self, game_data, cards): """Checks that the player has the right to play what he wants to @param game_data: Game data @param cards: cards the player want to play @return forbidden_cards cards or empty list if cards are ok""" forbidden_cards = [] if game_data['stage'] == 'ecart': for card in cards: if card.bout or card.value == "roi": forbidden_cards.append(card) # TODO: manage case where atouts (trumps) are in the dog elif game_data['stage'] == 'play': biggest_atout = None suit_asked = None players = game_data['players'] players_data = game_data['players_data'] idx = players.index(game_data['first_player']) current_idx = game_data['current_player'] current_player = players[current_idx] if idx == current_idx: # the player is the first to play, he can play what he wants return forbidden_cards while (idx != current_idx): player = players[idx] played_card = players_data[player]['played'] if not suit_asked and played_card.value != "excuse": suit_asked = played_card.suit if played_card.suit == "atout" and played_card > biggest_atout: biggest_atout = played_card idx = (idx + 1) % len(players) has_suit = False # True if there is one card of the asked suit in the hand of the player has_atout = False biggest_hand_atout = None for hand_card in game_data['hand'][current_player]: if hand_card.suit == suit_asked: has_suit = True if hand_card.suit == "atout": has_atout = True if hand_card.suit == "atout" and hand_card > biggest_hand_atout: biggest_hand_atout = hand_card assert len(cards) == 1 card = cards[0] if card.suit != suit_asked and has_suit and card.value != "excuse": forbidden_cards.append(card) return forbidden_cards if card.suit != suit_asked and card.suit != "atout" and has_atout: forbidden_cards.append(card) return forbidden_cards if card.suit == "atout" and card < biggest_atout and biggest_hand_atout > biggest_atout and card.value != "excuse": forbidden_cards.append(card) else: log.error(_('Internal error: unmanaged game stage')) return forbidden_cards def __start_play(self, room_jid, game_data, profile): """Start the game (tell to the first player after dealer to play""" game_data['stage'] = "play" next_player_idx = game_data['current_player'] = (game_data['init_player'] + 1) % len(game_data['players']) # the player after the dealer start game_data['first_player'] = next_player = game_data['players'][next_player_idx] to_jid = jid.JID(room_jid.userhost() + "/" + next_player) # FIXME: gof: self.send(to_jid, 'your_turn', profile=profile) def _contratChoosed(self, raw_data, profile): """Will be called when the contrat is selected @param raw_data: contains the choosed session id and the chosen contrat @param profile_key: profile """ try: session_data = self._sessions.profileGet(raw_data["session_id"], profile) except KeyError: log.warning(_("session id doesn't exist, session has probably expired")) # TODO: send error dialog return defer.succeed({}) room_jid = session_data['room_jid'] referee_jid = self.games[room_jid]['referee'] player = self.host.plugins["XEP-0045"].getRoomNick(room_jid, profile) data = xml_tools.XMLUIResult2DataFormResult(raw_data) contrat = data['contrat'] log.debug(_(u'contrat [%(contrat)s] choosed by %(profile)s') % {'contrat': contrat, 'profile': profile}) d = self.send(referee_jid, ('', 'contrat_choosed'), {'player': player}, content=contrat, profile=profile) d.addCallback(lambda ignore: {}) del self._sessions[raw_data["session_id"]] return d def _scoreShowed(self, raw_data, profile): """Will be called when the player closes the score dialog @param raw_data: nothing to retrieve from here but the session id @param profile_key: profile """ try: session_data = self._sessions.profileGet(raw_data["session_id"], profile) except KeyError: log.warning(_("session id doesn't exist, session has probably expired")) # TODO: send error dialog return defer.succeed({}) room_jid_s = session_data['room_jid'].userhost() # XXX: empty hand means to the frontend "reset the display"... self.host.bridge.tarotGameNew(room_jid_s, [], profile) del self._sessions[raw_data["session_id"]] return defer.succeed({}) def play_cards(self, player, referee, cards, profile_key=C.PROF_KEY_NONE): """Must be call by player when the contrat is selected @param player: player's name @param referee: arbiter jid @cards: cards played (list of tuples) @profile_key: profile """ profile = self.host.memory.getProfileName(profile_key) if not profile: log.error(_(u"profile %s is unknown") % profile_key) return log.debug(_(u'Cards played by %(profile)s: [%(cards)s]') % {'profile': profile, 'cards': cards}) elem = self.__card_list_to_xml(TarotCard.from_tuples(cards), 'cards_played') self.send(jid.JID(referee), elem, {'player': player}, profile=profile) def newRound(self, room_jid, profile): game_data = self.games[room_jid] players = game_data['players'] game_data['first_player'] = None # first player for the current trick game_data['contrat'] = None common_data = {'contrat': None, 'levees': [], # cards won 'played': None, # card on the table 'wait_for_low': None # Used when a player wait for a low card because of excuse } hand = game_data['hand'] = {} hand_size = game_data['hand_size'] chien = game_data['chien'] = [] deck = self.deck_ordered[:] random.shuffle(deck) for i in range(4): hand[players[i]] = deck[0:hand_size] del deck[0:hand_size] chien.extend(deck) del(deck[:]) msg_elts = {} for player in players: msg_elts[player] = self.__card_list_to_xml(hand[player], 'hand') RoomGame.newRound(self, room_jid, (common_data, msg_elts), profile) pl_idx = game_data['current_player'] = (game_data['init_player'] + 1) % len(players) # the player after the dealer start player = players[pl_idx] to_jid = jid.JID(room_jid.userhost() + "/" + player) # FIXME: gof: self.send(to_jid, self.__ask_contrat(), profile=profile) def room_game_cmd(self, mess_elt, profile): """ @param mess_elt: instance of twisted.words.xish.domish.Element """ client = self.host.getClient(profile) from_jid = jid.JID(mess_elt['from']) room_jid = jid.JID(from_jid.userhost()) nick = self.host.plugins["XEP-0045"].getRoomNick(client, room_jid) game_elt = mess_elt.firstChildElement() game_data = self.games[room_jid] is_player = self.isPlayer(room_jid, nick) if 'players_data' in game_data: players_data = game_data['players_data'] for elt in game_elt.elements(): if not is_player and (elt.name not in ('started', 'players')): continue # user is in the room but not playing if elt.name in ('started', 'players'): # new game created and/or players list updated players = [] for player in elt.elements(): players.append(unicode(player)) signal = self.host.bridge.tarotGameStarted if elt.name == 'started' else self.host.bridge.tarotGamePlayers signal(room_jid.userhost(), from_jid.full(), players, profile) elif elt.name == 'player_ready': # ready to play player = elt['player'] status = self.games[room_jid]['status'] nb_players = len(self.games[room_jid]['players']) status[player] = 'ready' log.debug(_(u'Player %(player)s is ready to start [status: %(status)s]') % {'player': player, 'status': status}) if status.values().count('ready') == nb_players: # everybody is ready, we can start the game self.newRound(room_jid, profile) elif elt.name == 'hand': # a new hand has been received self.host.bridge.tarotGameNew(room_jid.userhost(), self.__xml_to_list(elt), profile) elif elt.name == 'contrat': # it's time to choose contrat form = data_form.Form.fromElement(elt.firstChildElement()) session_id, session_data = self._sessions.newSession(profile=profile) session_data["room_jid"] = room_jid xml_data = xml_tools.dataForm2XMLUI(form, self.__choose_contrat_id, session_id).toXml() self.host.bridge.tarotGameChooseContrat(room_jid.userhost(), xml_data, profile) elif elt.name == 'contrat_choosed': # TODO: check we receive the contrat from the right person # TODO: use proper XEP-0004 way for answering form player = elt['player'] players_data[player]['contrat'] = unicode(elt) contrats = [players_data[p]['contrat'] for p in game_data['players']] if contrats.count(None): # not everybody has choosed his contrat, it's next one turn player = self.__next_player(game_data) to_jid = jid.JID(room_jid.userhost() + "/" + player) # FIXME: gof: self.send(to_jid, self.__ask_contrat(), profile=profile) else: best_contrat = [None, "Passe"] for player in game_data['players']: contrat = players_data[player]['contrat'] idx_best = self.contrats.index(best_contrat[1]) idx_pl = self.contrats.index(contrat) if idx_pl > idx_best: best_contrat[0] = player best_contrat[1] = contrat if best_contrat[1] == "Passe": log.debug(_("Everybody is passing, round ended")) to_jid = jid.JID(room_jid.userhost()) self.send(to_jid, self.__give_scores(*self.__draw_game(game_data)), profile=profile) game_data['init_player'] = (game_data['init_player'] + 1) % len(game_data['players']) # we change the dealer for player in game_data['players']: game_data['status'][player] = "init" return log.debug(_(u"%(player)s win the bid with %(contrat)s") % {'player': best_contrat[0], 'contrat': best_contrat[1]}) game_data['contrat'] = best_contrat[1] if game_data['contrat'] == "Garde Sans" or game_data['contrat'] == "Garde Contre": self.__start_play(room_jid, game_data, profile) game_data['attaquant'] = best_contrat[0] else: # Time to show the chien to everybody to_jid = jid.JID(room_jid.userhost()) # FIXME: gof: elem = self.__card_list_to_xml(game_data['chien'], 'chien') self.send(to_jid, elem, {'attaquant': best_contrat[0]}, profile=profile) # the attacker (attaquant) get the chien game_data['hand'][best_contrat[0]].extend(game_data['chien']) del game_data['chien'][:] if game_data['contrat'] == "Garde Sans": # The chien go into attaquant's (attacker) levees players_data[best_contrat[0]]['levees'].extend(game_data['chien']) del game_data['chien'][:] elif elt.name == 'chien': # we have received the chien log.debug(_("tarot: chien received")) data = {"attaquant": elt['attaquant']} game_data['stage'] = "ecart" game_data['attaquant'] = elt['attaquant'] self.host.bridge.tarotGameShowCards(room_jid.userhost(), "chien", self.__xml_to_list(elt), data, profile) elif elt.name == 'cards_played': if game_data['stage'] == "ecart": # TODO: show atouts (trumps) if player put some in écart assert (game_data['attaquant'] == elt['player']) # TODO: throw an xml error here list_cards = TarotCard.from_tuples(self.__xml_to_list(elt)) # we now check validity of card invalid_cards = self.__invalid_cards(game_data, list_cards) if invalid_cards: elem = self.__invalid_cards_elt(list_cards, invalid_cards, game_data['stage']) self.send(jid.JID(room_jid.userhost() + '/' + elt['player']), elem, profile=profile) return # FIXME: gof: manage Garde Sans & Garde Contre cases players_data[elt['player']]['levees'].extend(list_cards) # we add the chien to attaquant's levées for card in list_cards: game_data['hand'][elt['player']].remove(card) self.__start_play(room_jid, game_data, profile) elif game_data['stage'] == "play": current_player = game_data['players'][game_data['current_player']] cards = TarotCard.from_tuples(self.__xml_to_list(elt)) if mess_elt['type'] == 'groupchat': self.host.bridge.tarotGameCardsPlayed(room_jid.userhost(), elt['player'], self.__xml_to_list(elt), profile) else: # we first check validity of card invalid_cards = self.__invalid_cards(game_data, cards) if invalid_cards: elem = self.__invalid_cards_elt(cards, invalid_cards, game_data['stage']) self.send(jid.JID(room_jid.userhost() + '/' + current_player), elem, profile=profile) return # the card played is ok, we forward it to everybody # first we remove it from the hand and put in on the table game_data['hand'][current_player].remove(cards[0]) players_data[current_player]['played'] = cards[0] # then we forward the message self.send(room_jid, elt, profile=profile) # Did everybody played ? played = [players_data[player]['played'] for player in game_data['players']] if all(played): # everybody has played winner = self.__winner(game_data) log.debug(_(u'The winner of this trick is %s') % winner) # the winner win the trick self.__excuse_hack(game_data, played, winner) players_data[elt['player']]['levees'].extend(played) # nothing left on the table for player in game_data['players']: players_data[player]['played'] = None if len(game_data['hand'][current_player]) == 0: # no card left: the game is finished elem = self.__give_scores(*self.__calculate_scores(game_data)) self.send(room_jid, elem, profile=profile) game_data['init_player'] = (game_data['init_player'] + 1) % len(game_data['players']) # we change the dealer for player in game_data['players']: game_data['status'][player] = "init" return # next player is the winner next_player = game_data['first_player'] = self.__next_player(game_data, winner) else: next_player = self.__next_player(game_data) # finally, we tell to the next player to play to_jid = jid.JID(room_jid.userhost() + "/" + next_player) self.send(to_jid, 'your_turn', profile=profile) elif elt.name == 'your_turn': self.host.bridge.tarotGameYourTurn(room_jid.userhost(), profile) elif elt.name == 'score': form_elt = elt.elements(name='x', uri='jabber:x:data').next() winners = [] loosers = [] for winner in elt.elements(name='winner', uri=NS_CG): winners.append(unicode(winner)) for looser in elt.elements(name='looser', uri=NS_CG): loosers.append(unicode(looser)) form = data_form.Form.fromElement(form_elt) session_id, session_data = self._sessions.newSession(profile=profile) session_data["room_jid"] = room_jid xml_data = xml_tools.dataForm2XMLUI(form, self.__score_id, session_id).toXml() self.host.bridge.tarotGameScore(room_jid.userhost(), xml_data, winners, loosers, profile) elif elt.name == 'error': if elt['type'] == 'invalid_cards': played_cards = self.__xml_to_list(elt.elements(name='played', uri=NS_CG).next()) invalid_cards = self.__xml_to_list(elt.elements(name='invalid', uri=NS_CG).next()) self.host.bridge.tarotGameInvalidCards(room_jid.userhost(), elt['phase'], played_cards, invalid_cards, profile) else: log.error(_(u'Unmanaged error type: %s') % elt['type']) else: log.error(_(u'Unmanaged card game element: %s') % elt.name) def getSyncDataForPlayer(self, room_jid, nick): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_merge_req_mercurial.py0000755000175500017600000001500013243470024024202 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT plugin for import external blogs # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core import exceptions from twisted.internet import reactor, defer, protocol from twisted.python.failure import Failure from twisted.python.procutils import which from sat.core.log import getLogger log = getLogger(__name__) PLUGIN_INFO = { C.PI_NAME: "Mercurial Merge Request handler", C.PI_IMPORT_NAME: "MERGE_REQUEST_MERCURIAL", C.PI_TYPE: C.PLUG_TYPE_MISC, C.PI_DEPENDENCIES: ["MERGE_REQUESTS"], C.PI_MAIN: "MercurialHandler", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _(u"""Merge request handler for Mercurial""") } SHORT_DESC = D_(u"handle Mercurial repository") class MercurialProtocol(protocol.ProcessProtocol): """handle hg commands""" hg = None def __init__(self, deferred): self._deferred = deferred self.data = [] def outReceived(self, data): self.data.append(data) def errReceived(self, data): self.data.append(data) def processEnded(self, reason): data = u''.join([d.decode('utf-8') for d in self.data]) if (reason.value.exitCode == 0): log.debug(_('Mercurial command succeed')) self._deferred.callback(data) else: log.error(_(u"Can't complete Mercurial command (error code: {code}): {message}").format( code = reason.value.exitCode, message = data)) self._deferred.errback(Failure(RuntimeError)) @classmethod def run(cls, path, command, *args): """Create a new MercurialRegisterProtocol and execute the given mercurialctl command. @param path(unicode): path to the repository @param command(unicode): command to run @param *args(unicode): command arguments @return ((D)): """ d = defer.Deferred() mercurial_prot = MercurialProtocol(d) cmd_args = [cls.hg, command.encode('utf-8')] cmd_args.extend([a.encode('utf-8') for a in args]) reactor.spawnProcess(mercurial_prot, cls.hg, cmd_args, path=path.encode('utf-8')) return d class MercurialHandler(object): def __init__(self, host): log.info(_(u"Mercurial merge request handler initialization")) try: MercurialProtocol.hg = which('hg')[0] except IndexError: raise exceptions.NotFound(_(u"Mercurial executable (hg) not found, can't use Mercurial handler")) self.host = host self._m = host.plugins['MERGE_REQUESTS'] self._m.register('mercurial', self, [u'mercurial_changeset'], SHORT_DESC) def check(self, repository): d = MercurialProtocol.run(repository, 'identify') d.addCallback(lambda dummy: True) d.addErrback(lambda dummy: False) return d def export(self, repository): return MercurialProtocol.run(repository, 'export', '-g', '-r', 'outgoing()', '--encoding=utf-8') def parse(self, data, data_type=None): lines = data.splitlines() total_lines = len(lines) patches = [] while lines: patch = {} commit_msg = [] diff = [] state = 'init' if lines[0] != '# HG changeset patch': raise exceptions.DataError(_(u'invalid changeset signature')) # line index of this patch in the whole data patch_idx = total_lines - len(lines) del lines[0] for idx, line in enumerate(lines): if state == 'init': if line.startswith(u'# '): if line.startswith(u'# User '): elems = line[7:].split() if not elems: continue last = elems[-1] if last.startswith(u'<') and last.endswith(u'>') and u'@' in last: patch[self._m.META_EMAIL] = elems.pop()[1:-1] patch[self._m.META_AUTHOR] = u' '.join(elems) elif line.startswith(u'# Date '): time_data = line[7:].split() if len(time_data) != 2: log.warning(_(u'unexpected time data: {data}').format(data=line[7:])) continue patch[self._m.META_TIMESTAMP] = int(time_data[0]) + int(time_data[1]) elif line.startswith(u'# Node ID '): patch[self._m.META_HASH] = line[10:] elif line.startswith(u'# Parent '): patch[self._m.META_PARENT_HASH] = line[10:] else: state = 'commit_msg' if state == 'commit_msg': if line.startswith(u'diff --git a/'): state = 'diff' patch[self._m.META_DIFF_IDX] = patch_idx + idx + 1 else: commit_msg.append(line) if state == 'diff': if line.startswith(u'# ') or idx == len(lines)-1: # a new patch is starting or we have reached end of patches patch[self._m.META_COMMIT_MSG] = u'\n'.join(commit_msg) patch[self._m.META_DIFF] = u'\n'.join(diff) patches.append(patch) if idx == len(lines)-1: del lines[:] else: del lines[:idx] break else: diff.append(line) return patches sat-0.6.1.1+hg20180208/src/plugins/plugin_tickets_import_bugzilla.py0000755000175500017600000001217313243470025025133 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT plugin for import external blogs # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions # from twisted.internet import threads from twisted.internet import defer import os.path from lxml import etree from sat.tools import utils PLUGIN_INFO = { C.PI_NAME: "Bugzilla import", C.PI_IMPORT_NAME: "IMPORT_BUGZILLA", C.PI_TYPE: C.PLUG_TYPE_BLOG, C.PI_DEPENDENCIES: ["TICKETS_IMPORT"], C.PI_MAIN: "BugzillaImport", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Tickets importer for Bugzilla""") } SHORT_DESC = D_(u"import tickets from Bugzilla xml export file") LONG_DESC = D_(u"""This importer handle Bugzilla xml export file. To use it, you'll need to export tickets using XML. Tickets will be uploaded with the same ID as for Bugzilla, any existing ticket with this ID will be replaced. location: you must use the absolute path to your .xml file """) STATUS_MAP = { 'NEW': 'queued', 'ASSIGNED': 'started', 'RESOLVED': 'review', 'CLOSED': 'closed', 'REOPENED': 'started' # we loose data here because there is no need on basic workflow to have a reopened status } class BugzillaParser(object): # TODO: add a way to reassign values def parse(self, file_path): tickets = [] root = etree.parse(file_path) for bug in root.xpath('bug'): ticket = {} ticket['id'] = bug.findtext('bug_id') ticket['created'] = utils.date_parse(bug.findtext('creation_ts')) ticket['updated'] = utils.date_parse(bug.findtext('delta_ts')) ticket['title'] = bug.findtext('short_desc') reporter_elt = bug.find('reporter') ticket['author'] = reporter_elt.get('name') if ticket['author'] is None: if '@' in reporter_elt.text: ticket['author'] = reporter_elt.text[:reporter_elt.text.find('@')].title() else: ticket['author'] = u'no name' ticket['author_email'] = reporter_elt.text assigned_to_elt = bug.find('assigned_to') ticket['assigned_to_name'] = assigned_to_elt.get('name') ticket['assigned_to_email'] = assigned_to_elt.text ticket['cc_emails'] = [e.text for e in bug.findall('cc')] ticket['priority'] = bug.findtext('priority').lower().strip() ticket['severity'] = bug.findtext('bug_severity').lower().strip() ticket['product'] = bug.findtext('product') ticket['component'] = bug.findtext('component') ticket['version'] = bug.findtext('version') ticket['platform'] = bug.findtext('rep_platform') ticket['os'] = bug.findtext('op_sys') ticket['status'] = STATUS_MAP.get(bug.findtext('bug_status'), 'queued') ticket['milestone'] = bug.findtext('target_milestone') body = None comments = [] for longdesc in bug.findall('long_desc'): if body is None: body = longdesc.findtext('thetext') else: who = longdesc.find('who') comment = {'id': longdesc.findtext('commentid'), 'author_email': who.text, 'published': utils.date_parse(longdesc.findtext('bug_when')), 'author': who.get('name', who.text), 'content': longdesc.findtext('thetext')} comments.append(comment) ticket['body'] = body ticket['comments'] = comments tickets.append(ticket) tickets.sort(key = lambda t: int(t['id'])) return (tickets, len(tickets)) class BugzillaImport(object): def __init__(self, host): log.info(_(u"Bugilla Import plugin initialization")) self.host = host host.plugins['TICKETS_IMPORT'].register('bugzilla', self.Import, SHORT_DESC, LONG_DESC) def Import(self, client, location, options=None): if not os.path.isabs(location): raise exceptions.DataError(u"An absolute path to XML data need to be given as location") bugzilla_parser = BugzillaParser() # d = threads.deferToThread(bugzilla_parser.parse, location) d = defer.maybeDeferred(bugzilla_parser.parse, location) return d sat-0.6.1.1+hg20180208/src/plugins/plugin_adhoc_dbus.py0000644000175500017600000002362713243470024022277 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for adding D-Bus to Ad-Hoc Commands # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from twisted.internet import defer from wokkel import data_form try: from lxml import etree except ImportError: raise exceptions.MissingModule(u"Missing module lxml, please download/install it from http://lxml.de/") import os.path import uuid import dbus from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) FD_NAME = "org.freedesktop.DBus" FD_PATH = "/org/freedekstop/DBus" INTROSPECT_IFACE = "org.freedesktop.DBus.Introspectable" INTROSPECT_METHOD = "Introspect" IGNORED_IFACES_START = ('org.freedesktop', 'org.qtproject', 'org.kde.KMainWindow') # commands in interface starting with these values will be ignored FLAG_LOOP = 'LOOP' PLUGIN_INFO = { C.PI_NAME: "Ad-Hoc Commands - D-Bus", C.PI_IMPORT_NAME: "AD_HOC_DBUS", C.PI_TYPE: "Misc", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0050"], C.PI_MAIN: "AdHocDBus", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Add D-Bus management to Ad-Hoc commands""") } class AdHocDBus(object): def __init__(self, host): log.info(_("plugin Ad-Hoc D-Bus initialization")) self.host = host host.bridge.addMethod("adHocDBusAddAuto", ".plugin", in_sign='sasasasasasass', out_sign='(sa(sss))', method=self._adHocDBusAddAuto, async=True) self.session_bus = dbus.SessionBus() self.fd_object = self.session_bus.get_object(FD_NAME, FD_PATH, introspect=False) self.XEP_0050 = host.plugins['XEP-0050'] def _DBusAsyncCall(self, proxy, method, *args, **kwargs): """ Call a DBus method asynchronously and return a deferred @param proxy: DBus object proxy, as returner by get_object @param method: name of the method to call @param args: will be transmitted to the method @param kwargs: will be transmetted to the method, except for the following poped values: - interface: name of the interface to use @return: a deferred """ d = defer.Deferred() interface = kwargs.pop('interface', None) kwargs['reply_handler'] = lambda ret=None: d.callback(ret) kwargs['error_handler'] = d.errback proxy.get_dbus_method(method, dbus_interface=interface)(*args, **kwargs) return d def _DBusListNames(self): return self._DBusAsyncCall(self.fd_object, "ListNames") def _DBusIntrospect(self, proxy): return self._DBusAsyncCall(proxy, INTROSPECT_METHOD, interface=INTROSPECT_IFACE) def _acceptMethod(self, method): """ Return True if we accept the method for a command @param method: etree.Element @return: True if the method is acceptable """ if method.xpath("arg[@direction='in']"): # we don't accept method with argument for the moment return False return True @defer.inlineCallbacks def _introspect(self, methods, bus_name, proxy): log.debug("introspecting path [%s]" % proxy.object_path) introspect_xml = yield self._DBusIntrospect(proxy) el = etree.fromstring(introspect_xml) for node in el.iterchildren('node', 'interface'): if node.tag == 'node': new_path = os.path.join(proxy.object_path, node.get('name')) new_proxy = self.session_bus.get_object(bus_name, new_path, introspect=False) yield self._introspect(methods, bus_name, new_proxy) elif node.tag == 'interface': name = node.get('name') if any(name.startswith(ignored) for ignored in IGNORED_IFACES_START): log.debug('interface [%s] is ignored' % name) continue log.debug("introspecting interface [%s]" % name) for method in node.iterchildren('method'): if self._acceptMethod(method): method_name = method.get('name') log.debug("method accepted: [%s]" % method_name) methods.add((proxy.object_path, name, method_name)) def _adHocDBusAddAuto(self, prog_name, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, flags, profile_key): return self.adHocDBusAddAuto(prog_name, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, flags, profile_key) @defer.inlineCallbacks def adHocDBusAddAuto(self, prog_name, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None, flags=None, profile_key=C.PROF_KEY_NONE): bus_names = yield self._DBusListNames() bus_names = [bus_name for bus_name in bus_names if '.' + prog_name in bus_name] if not bus_names: log.info("Can't find any bus for [%s]" % prog_name) defer.returnValue(("", [])) bus_names.sort() for bus_name in bus_names: if bus_name.endswith(prog_name): break log.info("bus name found: [%s]" % bus_name) proxy = self.session_bus.get_object(bus_name, '/', introspect=False) methods = set() yield self._introspect(methods, bus_name, proxy) if methods: self._addCommand(prog_name, bus_name, methods, allowed_jids = allowed_jids, allowed_groups = allowed_groups, allowed_magics = allowed_magics, forbidden_jids = forbidden_jids, forbidden_groups = forbidden_groups, flags = flags, profile_key = profile_key) defer.returnValue((bus_name, methods)) def _addCommand(self, adhoc_name, bus_name, methods, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None, flags=None, profile_key=C.PROF_KEY_NONE): if flags is None: flags = set() def DBusCallback(command_elt, session_data, action, node, profile): actions = session_data.setdefault('actions',[]) names_map = session_data.setdefault('names_map', {}) actions.append(action) if len(actions) == 1: # it's our first request, we ask the desired new status status = self.XEP_0050.STATUS.EXECUTING form = data_form.Form('form', title=_('Command selection')) options = [] for path, iface, command in methods: label = command.rsplit('.',1)[-1] name = str(uuid.uuid4()) names_map[name] = (path, iface, command) options.append(data_form.Option(name, label)) field = data_form.Field('list-single', 'command', options=options, required=True) form.addField(field) payload = form.toElement() note = None elif len(actions) == 2: # we should have the answer here try: x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next() answer_form = data_form.Form.fromElement(x_elt) command = answer_form['command'] except (KeyError, StopIteration): raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.BAD_PAYLOAD) if command not in names_map: raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.BAD_PAYLOAD) path, iface, command = names_map[command] proxy = self.session_bus.get_object(bus_name, path) self._DBusAsyncCall(proxy, command, interface=iface) # job done, we can end the session, except if we have FLAG_LOOP if FLAG_LOOP in flags: # We have a loop, so we clear everything and we execute again the command as we had a first call (command_elt is not used, so None is OK) del actions[:] names_map.clear() return DBusCallback(None, session_data, self.XEP_0050.ACTION.EXECUTE, node, profile) form = data_form.Form('form', title=_(u'Updated')) form.addField(data_form.Field('fixed', u'Command sent')) status = self.XEP_0050.STATUS.COMPLETED payload = None note = (self.XEP_0050.NOTE.INFO, _(u"Command sent")) else: raise self.XEP_0050.AdHocError(self.XEP_0050.ERROR.INTERNAL) return (payload, status, None, note) self.XEP_0050.addAdHocCommand(DBusCallback, adhoc_name, allowed_jids = allowed_jids, allowed_groups = allowed_groups, allowed_magics = allowed_magics, forbidden_jids = forbidden_jids, forbidden_groups = forbidden_groups, profile_key = profile_key) sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0077.py0000755000175500017600000002250213243470025021450 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing xep-0077 # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core import exceptions from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import xmlstream from twisted.internet import defer, reactor from sat.tools import xml_tools from wokkel import data_form NS_REG = 'jabber:iq:register' PLUGIN_INFO = { C.PI_NAME: "XEP 0077 Plugin", C.PI_IMPORT_NAME: "XEP-0077", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0077"], C.PI_DEPENDENCIES: [], C.PI_MAIN: "XEP_0077", C.PI_DESCRIPTION: _("""Implementation of in-band registration""") } # FIXME: this implementation is incomplete class RegisteringAuthenticator(xmlstream.ConnectAuthenticator): # FIXME: request IQ is not send to check available fields, while XEP recommand to use it # FIXME: doesn't handle data form or oob def __init__(self, jid_, password, email=None): xmlstream.ConnectAuthenticator.__init__(self, jid_.host) self.jid = jid_ self.password = password self.email = email self.registered = defer.Deferred() log.debug(_(u"Registration asked for {jid}").format( jid = jid_)) def connectionMade(self): log.debug(_(u"Connection made with {server}".format(server=self.jid.host))) self.xmlstream.otherEntity = jid.JID(self.jid.host) self.xmlstream.namespace = C.NS_CLIENT self.xmlstream.sendHeader() iq = XEP_0077.buildRegisterIQ(self.xmlstream, self.jid, self.password, self.email) d = iq.send(self.jid.host).addCallbacks(self.registrationCb, self.registrationEb) d.chainDeferred(self.registered) def registrationCb(self, answer): log.debug(_(u"Registration answer: {}").format(answer.toXml())) self.xmlstream.sendFooter() def registrationEb(self, failure_): log.info(_("Registration failure: {}").format(unicode(failure_.value))) self.xmlstream.sendFooter() raise failure_ class XEP_0077(object): def __init__(self, host): log.info(_("Plugin XEP_0077 initialization")) self.host = host host.bridge.addMethod("inBandRegister", ".plugin", in_sign='ss', out_sign='', method=self._inBandRegister, async=True) host.bridge.addMethod("inBandAccountNew", ".plugin", in_sign='ssssi', out_sign='', method=self._registerNewAccount, async=True) host.bridge.addMethod("inBandUnregister", ".plugin", in_sign='ss', out_sign='', method=self._unregister, async=True) host.bridge.addMethod("inBandPasswordChange", ".plugin", in_sign='ss', out_sign='', method=self._changePassword, async=True) @staticmethod def buildRegisterIQ(xmlstream_, jid_, password, email=None): iq_elt = xmlstream.IQ(xmlstream_, 'set') iq_elt["to"] = jid_.host query_elt = iq_elt.addElement(('jabber:iq:register', 'query')) username_elt = query_elt.addElement('username') username_elt.addContent(jid_.user) password_elt = query_elt.addElement('password') password_elt.addContent(password) if email is not None: email_elt = query_elt.addElement('email') email_elt.addContent(email) return iq_elt def _regCb(self, answer, client, post_treat_cb): """Called after the first get IQ""" try: query_elt = answer.elements(NS_REG, 'query').next() except StopIteration: raise exceptions.DataError("Can't find expected query element") try: x_elem = query_elt.elements(data_form.NS_X_DATA, 'x').next() except StopIteration: # XXX: it seems we have an old service which doesn't manage data forms log.warning(_("Can't find data form")) raise exceptions.DataError(_("This gateway can't be managed by SàT, sorry :(")) def submitForm(data, profile): form_elt = xml_tools.XMLUIResultToElt(data) iq_elt = client.IQ() iq_elt['id'] = answer['id'] iq_elt['to'] = answer['from'] query_elt = iq_elt.addElement("query", NS_REG) query_elt.addChild(form_elt) d = iq_elt.send() d.addCallback(self._regSuccess, client, post_treat_cb) d.addErrback(self._regFailure, client) return d form = data_form.Form.fromElement(x_elem) submit_reg_id = self.host.registerCallback(submitForm, with_data=True, one_shot=True) return xml_tools.dataForm2XMLUI(form, submit_reg_id) def _regEb(self, failure, client): """Called when something is wrong with registration""" log.info(_("Registration failure: %s") % unicode(failure.value)) raise failure def _regSuccess(self, answer, client, post_treat_cb): log.debug(_(u"registration answer: %s") % answer.toXml()) if post_treat_cb is not None: post_treat_cb(jid.JID(answer['from']), client.profile) return {} def _regFailure(self, failure, client): log.info(_(u"Registration failure: %s") % unicode(failure.value)) if failure.value.condition == 'conflict': raise exceptions.ConflictError( _("Username already exists, please choose an other one")) raise failure def _inBandRegister(self, to_jid_s, profile_key=C.PROF_KEY_NONE): return self.inBandRegister, jid.JID(to_jid_s, profile_key) def inBandRegister(self, to_jid, post_treat_cb=None, profile_key=C.PROF_KEY_NONE): """register to a service @param to_jid(jid.JID): jid of the service to register to """ # FIXME: this post_treat_cb arguments seems wrong, check it client = self.host.getClient(profile_key) log.debug(_(u"Asking registration for {}").format(to_jid.full())) reg_request = client.IQ(u'get') reg_request["from"] = client.jid.full() reg_request["to"] = to_jid.full() reg_request.addElement('query', NS_REG) d = reg_request.send(to_jid.full()).addCallbacks(self._regCb, self._regEb, callbackArgs=[client, post_treat_cb], errbackArgs=[client]) return d def _registerNewAccount(self, jid_, password, email, host, port): kwargs = {} if email: kwargs['email'] = email if host: kwargs['host'] = host if port: kwargs['port'] = port return self.registerNewAccount(jid.JID(jid_), password, **kwargs) def registerNewAccount(self, jid_, password, email=None, host=u"127.0.0.1", port=C.XMPP_C2S_PORT): """register a new account on a XMPP server @param jid_(jid.JID): request jid to register @param password(unicode): password of the account @param email(unicode): email of the account @param host(unicode): host of the server to register to @param port(int): port of the server to register to """ authenticator = RegisteringAuthenticator(jid_, password, email) registered_d = authenticator.registered serverRegistrer = xmlstream.XmlStreamFactory(authenticator) connector = reactor.connectTCP(host, port, serverRegistrer) serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect() return registered_d def _changePassword(self, new_password, profile_key): client = self.host.getClient(profile_key) return self.changePassword(client, new_password) def changePassword(self, client, new_password): iq_elt = self.buildRegisterIQ(client.xmlstream, client.jid, new_password) d = iq_elt.send(client.jid.host) d.addCallback(lambda dummy: self.host.memory.setParam("Password", new_password, "Connection", profile_key=client.profile)) return d def _unregister(self, to_jid_s, profile_key): client = self.host.getClient(profile_key) return self.unregister(client, jid.JID(to_jid_s)) def unregister(self, client, to_jid): """remove registration from a server/service BEWARE! if you remove registration from profile own server, this will DELETE THE XMPP ACCOUNT WITHOUT WARNING @param to_jid(jid.JID): jid of the service or server """ iq_elt = client.IQ() iq_elt['to'] = to_jid.full() query_elt = iq_elt.addElement((NS_REG, u'query')) query_elt.addElement(u'remove') return iq_elt.send() sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0060.py0000755000175500017600000013611713243470025021450 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Publish-Subscribe (xep-0060) # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from sat.tools import sat_defer from twisted.words.protocols.jabber import jid, error from twisted.internet import defer from wokkel import disco from wokkel import data_form from zope.interface import implements from collections import namedtuple import urllib import datetime from dateutil import tz # XXX: sat_tmp.wokkel.pubsub is actually use instead of wokkel version # mam and rsm come from sat_tmp.wokkel too from wokkel import pubsub from wokkel import rsm from wokkel import mam PLUGIN_INFO = { C.PI_NAME: "Publish-Subscribe", C.PI_IMPORT_NAME: "XEP-0060", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0060"], C.PI_DEPENDENCIES: [], C.PI_RECOMMENDATIONS: ["XEP-0313"], C.PI_MAIN: "XEP_0060", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of PubSub Protocol""") } UNSPECIFIED = "unspecified error" MAM_FILTER = "mam_filter_" Extra = namedtuple('Extra', ('rsm_request', 'extra')) # rsm_request is the rsm.RSMRequest build with rsm_ prefixed keys, or None # extra is a potentially empty dict class XEP_0060(object): OPT_ACCESS_MODEL = 'pubsub#access_model' OPT_PERSIST_ITEMS = 'pubsub#persist_items' OPT_MAX_ITEMS = 'pubsub#max_items' OPT_DELIVER_PAYLOADS = 'pubsub#deliver_payloads' OPT_SEND_ITEM_SUBSCRIBE = 'pubsub#send_item_subscribe' OPT_NODE_TYPE = 'pubsub#node_type' OPT_SUBSCRIPTION_TYPE = 'pubsub#subscription_type' OPT_SUBSCRIPTION_DEPTH = 'pubsub#subscription_depth' OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed' OPT_PUBLISH_MODEL = 'pubsub#publish_model' ACCESS_OPEN = 'open' ACCESS_PRESENCE = 'presence' ACCESS_ROSTER = 'roster' ACCESS_PUBLISHER_ROSTER = 'publisher-roster' ACCESS_AUTHORIZE = 'authorize' ACCESS_WHITELIST = 'whitelist' def __init__(self, host): log.info(_(u"PubSub plugin initialization")) self.host = host self._mam = host.plugins.get('XEP-0313') self._node_cb = {} # dictionnary of callbacks for node (key: node, value: list of callbacks) self.rt_sessions = sat_defer.RTDeferredSessions() host.bridge.addMethod("psNodeCreate", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._createNode, async=True) host.bridge.addMethod("psNodeConfigurationGet", ".plugin", in_sign='sss', out_sign='a{ss}', method=self._getNodeConfiguration, async=True) host.bridge.addMethod("psNodeConfigurationSet", ".plugin", in_sign='ssa{ss}s', out_sign='', method=self._setNodeConfiguration, async=True) host.bridge.addMethod("psNodeAffiliationsGet", ".plugin", in_sign='sss', out_sign='a{ss}', method=self._getNodeAffiliations, async=True) host.bridge.addMethod("psNodeAffiliationsSet", ".plugin", in_sign='ssa{ss}s', out_sign='', method=self._setNodeAffiliations, async=True) host.bridge.addMethod("psNodeSubscriptionsGet", ".plugin", in_sign='sss', out_sign='a{ss}', method=self._getNodeSubscriptions, async=True) host.bridge.addMethod("psNodeSubscriptionsSet", ".plugin", in_sign='ssa{ss}s', out_sign='', method=self._setNodeSubscriptions, async=True) host.bridge.addMethod("psNodeDelete", ".plugin", in_sign='sss', out_sign='', method=self._deleteNode, async=True) host.bridge.addMethod("psNodeWatchAdd", ".plugin", in_sign='sss', out_sign='', method=self._addWatch, async=False) host.bridge.addMethod("psNodeWatchRemove", ".plugin", in_sign='sss', out_sign='', method=self._removeWatch, async=False) host.bridge.addMethod("psAffiliationsGet", ".plugin", in_sign='sss', out_sign='a{ss}', method=self._getAffiliations, async=True) host.bridge.addMethod("psItemsGet", ".plugin", in_sign='ssiassa{ss}s', out_sign='(asa{ss})', method=self._getItems, async=True) host.bridge.addMethod("psItemSend", ".plugin", in_sign='ssssa{ss}s', out_sign='s', method=self._sendItem, async=True) host.bridge.addMethod("psRetractItem", ".plugin", in_sign='sssbs', out_sign='', method=self._retractItem, async=True) host.bridge.addMethod("psRetractItems", ".plugin", in_sign='ssasbs', out_sign='', method=self._retractItems, async=True) host.bridge.addMethod("psSubscribe", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._subscribe, async=True) host.bridge.addMethod("psUnsubscribe", ".plugin", in_sign='sss', out_sign='', method=self._unsubscribe, async=True) host.bridge.addMethod("psSubscriptionsGet", ".plugin", in_sign='sss', out_sign='aa{ss}', method=self._subscriptions, async=True) host.bridge.addMethod("psSubscribeToMany", ".plugin", in_sign='a(ss)sa{ss}s', out_sign='s', method=self._subscribeToMany) host.bridge.addMethod("psGetSubscribeRTResult", ".plugin", in_sign='ss', out_sign='(ua(sss))', method=self._manySubscribeRTResult, async=True) host.bridge.addMethod("psGetFromMany", ".plugin", in_sign='a(ss)ia{ss}s', out_sign='s', method=self._getFromMany) host.bridge.addMethod("psGetFromManyRTResult", ".plugin", in_sign='ss', out_sign='(ua(sssasa{ss}))', method=self._getFromManyRTResult, async=True) # high level observer method host.bridge.addSignal("psEvent", ".plugin", signature='ssssa{ss}s') # args: category, service(jid), node, type (C.PS_ITEMS, C.PS_DELETE), data, profile # low level observer method, used if service/node is in watching list (see psNodeWatch* methods) host.bridge.addSignal("psEventRaw", ".plugin", signature='sssass') # args: service(jid), node, type (C.PS_ITEMS, C.PS_DELETE), list of item_xml, profile def getHandler(self, client): client.pubsub_client = SatPubSubClient(self.host, self) return client.pubsub_client @defer.inlineCallbacks def profileConnected(self, client): client.pubsub_watching = set() try: client.pubsub_service = jid.JID(self.host.memory.getConfig('', 'pubsub_service')) except RuntimeError: log.info(_(u"Can't retrieve pubsub_service from conf, we'll use first one that we find")) client.pubsub_service = yield self.host.findServiceEntity(client, "pubsub", "service") def getFeatures(self, profile): try: client = self.host.getClient(profile) except exceptions.ProfileNotSetError: return {} try: return {'service': client.pubsub_service.full() if client.pubsub_service is not None else ''} except AttributeError: if self.host.isConnected(profile): log.debug("Profile is not connected, service is not checked yet") else: log.error("Service should be available !") return {} def parseExtra(self, extra): """Parse extra dictionnary used bridge's extra dictionnaries @param extra(dict): extra data used to configure request @return(Extra): filled Extra instance """ if extra is None: rsm_request = None extra = {} else: # rsm rsm_args = {} for arg in ('max', 'after', 'before', 'index'): try: argname = "max_" if arg == 'max' else arg rsm_args[argname] = extra.pop('rsm_{}'.format(arg)) except KeyError: continue if rsm_args: rsm_request = rsm.RSMRequest(**rsm_args) else: rsm_request = None # mam mam_args = {} for arg in ('start', 'end'): try: mam_args[arg] = datetime.datetime.fromtimestamp(int(extra.pop('{}{}'.format(MAM_FILTER, arg))), tz.tzutc()) except (TypeError, ValueError): log.warning(u"Bad value for {} filter".format(arg)) except KeyError: continue try: mam_args['with_jid'] = jid.JID(extra.pop('{}jid'.format(MAM_FILTER))) except (jid.InvalidFormat): log.warning(u"Bad value for jid filter") except KeyError: pass for name, value in extra.iteritems(): if name.startswith(MAM_FILTER): var = name[len(MAM_FILTER):] extra_fields = mam_args.setdefault('extra_fields', []) extra_fields.append(data_form.Field(var=var, value=value)) if mam_args: assert 'mam' not in extra extra['mam'] = mam.MAMRequest(mam.buildForm(**mam_args)) return Extra(rsm_request, extra) def addManagedNode(self, node, **kwargs): """Add a handler for a node @param node(unicode): node to monitor all node *prefixed* with this one will be triggered @param **kwargs: method(s) to call when the node is found the method must be named after PubSub constants in lower case and suffixed with "_cb" e.g.: "items_cb" for C.PS_ITEMS, "delete_cb" for C.PS_DELETE """ assert node is not None assert kwargs callbacks = self._node_cb.setdefault(node, {}) for event, cb in kwargs.iteritems(): event_name = event[:-3] assert event_name in C.PS_EVENTS callbacks.setdefault(event_name,[]).append(cb) def removeManagedNode(self, node, *args): """Add a handler for a node @param node(unicode): node to monitor @param *args: callback(s) to remove """ assert args try: registred_cb = self._node_cb[node] except KeyError: pass else: for callback in args: for event, cb_list in registred_cb.iteritems(): try: cb_list.remove(callback) except ValueError: pass else: log.debug(u"removed callback {cb} for event {event} on node {node}".format( cb=callback, event=event, node=node)) if not cb_list: del registred_cb[event] if not registred_cb: del self._node_cb[node] return log.error(u"Trying to remove inexistant callback {cb} for node {node}".format(cb=callback, node=node)) # def listNodes(self, service, nodeIdentifier='', profile=C.PROF_KEY_NONE): # """Retrieve the name of the nodes that are accessible on the target service. # @param service (JID): target service # @param nodeIdentifier (str): the parent node name (leave empty to retrieve first-level nodes) # @param profile (str): %(doc_profile)s # @return: deferred which fire a list of nodes # """ # client = self.host.getClient(profile) # d = self.host.getDiscoItems(client, service, nodeIdentifier) # d.addCallback(lambda result: [item.getAttribute('node') for item in result.toElement().children if item.hasAttribute('node')]) # return d # def listSubscribedNodes(self, service, nodeIdentifier='', filter_='subscribed', profile=C.PROF_KEY_NONE): # """Retrieve the name of the nodes to which the profile is subscribed on the target service. # @param service (JID): target service # @param nodeIdentifier (str): the parent node name (leave empty to retrieve all subscriptions) # @param filter_ (str): filter the result according to the given subscription type: # - None: do not filter # - 'pending': subscription has not been approved yet by the node owner # - 'unconfigured': subscription options have not been configured yet # - 'subscribed': subscription is complete # @param profile (str): %(doc_profile)s # @return: Deferred list[str] # """ # d = self.subscriptions(service, nodeIdentifier, profile_key=profile) # d.addCallback(lambda subs: [sub.getAttribute('node') for sub in subs if sub.getAttribute('subscription') == filter_]) # return d def _sendItem(self, service, nodeIdentifier, payload, item_id=None, extra=None, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) service = None if not service else jid.JID(service) d = self.sendItem(client, service, nodeIdentifier, payload, item_id or None, extra) d.addCallback(lambda ret: ret or u'') return d def _getPublishedItemId(self, iq_elt, original_id): """return item of published id if found in answer if not found original_id is returned, or empty string if it is None or empty string """ try: item_id = iq_elt.pubsub.publish.item['id'] except (AttributeError, KeyError): item_id = None return item_id or original_id def sendItem(self, client, service, nodeIdentifier, payload, item_id=None, extra=None): """high level method to send one item @param service(jid.JID, None): service to send the item to None to use PEP @param NodeIdentifier(unicode): PubSub node to use @param item_id(unicode, None): id to use or None to create one @param payload(domish.Element, unicode): payload of the item to send @param extra(dict, None): extra option, not used yet @return (unicode, None): id of the created item """ item_elt = pubsub.Item(id=item_id, payload=payload) d = self.publish(client, service, nodeIdentifier, [item_elt]) d.addCallback(self._getPublishedItemId, item_id) return d def publish(self, client, service, nodeIdentifier, items=None): return client.pubsub_client.publish(service, nodeIdentifier, items, client.pubsub_client.parent.jid) def _unwrapMAMMessage(self, message_elt): try: item_elt = (message_elt.elements(mam.NS_MAM, 'result').next() .elements(C.NS_FORWARD, 'forwarded').next() .elements(C.NS_CLIENT, 'message').next() .elements('http://jabber.org/protocol/pubsub#event', 'event').next() .elements('http://jabber.org/protocol/pubsub#event', 'items').next() .elements('http://jabber.org/protocol/pubsub#event', 'item').next()) except StopIteration: raise exceptions.DataError(u"Can't find Item in MAM message element") return item_elt def _getItems(self, service='', node='', max_items=10, item_ids=None, sub_id=None, extra_dict=None, profile_key=C.PROF_KEY_NONE): """Get items from pubsub node @param max_items(int): maximum number of item to get, C.NO_LIMIT for no limit """ client = self.host.getClient(profile_key) service = jid.JID(service) if service else None max_items = None if max_items == C.NO_LIMIT else max_items extra = self.parseExtra(extra_dict) d = self.getItems(client, service, node or None, max_items or None, item_ids, sub_id or None, extra.rsm_request, extra.extra) d.addCallback(self.serItemsData) return d def getItems(self, client, service, node, max_items=None, item_ids=None, sub_id=None, rsm_request=None, extra=None): """Retrieve pubsub items from a node. @param service (JID, None): pubsub service. @param node (str): node id. @param max_items (int): optional limit on the number of retrieved items. @param item_ids (list[str]): identifiers of the items to be retrieved (can't be used with rsm_request). @param sub_id (str): optional subscription identifier. @param rsm_request (rsm.RSMRequest): RSM request data @return: a deferred couple (list[dict], dict) containing: - list of items - metadata with the following keys: - rsm_first, rsm_last, rsm_count, rsm_index: first, last, count and index value of RSMResponse - service, node: service and node used """ if item_ids and max_items is not None: max_items = None if rsm_request and item_ids: raise ValueError(u"items_id can't be used with rsm") if extra is None: extra = {} try: mam_query = extra['mam'] except KeyError: d = client.pubsub_client.items(service, node, max_items, item_ids, sub_id, None, rsm_request) else: # if mam is requested, we have to do a totally different query if self._mam is None: raise exceptions.NotFound(u"MAM (XEP-0313) plugin is not available") if max_items is not None: raise exceptions.DataError(u"max_items parameter can't be used with MAM") if item_ids: raise exceptions.DataError(u"items_ids parameter can't be used with MAM") if mam_query.node is None: mam_query.node = node elif mam_query.node != node: raise exceptions.DataError(u"MAM query node is incoherent with getItems's node") if mam_query.rsm is None: mam_query.rsm = rsm_request else: if mam_query.rsm != rsm_request: raise exceptions.DataError(u"Conflict between RSM request and MAM's RSM request") d = self._mam.getArchives(client, mam_query, service, self._unwrapMAMMessage) try: subscribe = C.bool(extra['subscribe']) except KeyError: subscribe = False def subscribeEb(failure, service, node): failure.trap(error.StanzaError) log.warning(u"Could not subscribe to node {} on service {}: {}".format(node, unicode(service), unicode(failure.value))) def doSubscribe(items): self.subscribe(service, node, profile_key=client.profile).addErrback(subscribeEb, service, node) return items if subscribe: d.addCallback(doSubscribe) def addMetadata(result): items, rsm_response = result service_jid = service if service else client.jid.userhostJID() metadata = {'service': service_jid, 'node': node, 'uri': self.getNodeURI(service_jid, node), } if rsm_request is not None and rsm_response is not None: metadata.update({'rsm_{}'.format(key): value for key, value in rsm_response.toDict().iteritems()}) return (items, metadata) d.addCallback(addMetadata) return d # @defer.inlineCallbacks # def getItemsFromMany(self, service, data, max_items=None, sub_id=None, rsm=None, profile_key=C.PROF_KEY_NONE): # """Massively retrieve pubsub items from many nodes. # @param service (JID): target service. # @param data (dict): dictionnary binding some arbitrary keys to the node identifiers. # @param max_items (int): optional limit on the number of retrieved items *per node*. # @param sub_id (str): optional subscription identifier. # @param rsm (dict): RSM request data # @param profile_key (str): %(doc_profile_key)s # @return: a deferred dict with: # - key: a value in (a subset of) data.keys() # - couple (list[dict], dict) containing: # - list of items # - RSM response data # """ # client = self.host.getClient(profile_key) # found_nodes = yield self.listNodes(service, profile=client.profile) # d_dict = {} # for publisher, node in data.items(): # if node not in found_nodes: # log.debug(u"Skip the items retrieval for [{node}]: node doesn't exist".format(node=node)) # continue # avoid pubsub "item-not-found" error # d_dict[publisher] = self.getItems(service, node, max_items, None, sub_id, rsm, client.profile) # defer.returnValue(d_dict) def getOptions(self, service, nodeIdentifier, subscriber, subscriptionIdentifier=None, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) return client.pubsub_client.getOptions(service, nodeIdentifier, subscriber, subscriptionIdentifier) def setOptions(self, service, nodeIdentifier, subscriber, options, subscriptionIdentifier=None, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) return client.pubsub_client.setOptions(service, nodeIdentifier, subscriber, options, subscriptionIdentifier) def _createNode(self, service_s, nodeIdentifier, options, profile_key): client = self.host.getClient(profile_key) return self.createNode(client, jid.JID(service_s) if service_s else None, nodeIdentifier, options) def createNode(self, client, service, nodeIdentifier=None, options=None): """Create a new node @param service(jid.JID): PubSub service, @param NodeIdentifier(unicode, None): node name use None to create instant node (identifier will be returned by this method) @param option(dict[unicode, unicode], None): node configuration options @return (unicode): identifier of the created node (may be different from requested name) """ # TODO: if pubsub service doesn't hande publish-options, configure it in a second time return client.pubsub_client.createNode(service, nodeIdentifier, options) @defer.inlineCallbacks def createIfNewNode(self, client, service, nodeIdentifier, options=None): """Helper method similar to createNode, but will not fail in case of conflict""" try: yield self.createNode(client, service, nodeIdentifier, options) except error.StanzaError as e: if e.condition == 'conflict': pass else: raise e def _getNodeConfiguration(self, service_s, nodeIdentifier, profile_key): client = self.host.getClient(profile_key) d = self.getConfiguration(client, jid.JID(service_s) if service_s else None, nodeIdentifier) def serialize(form): # FIXME: better more generic dataform serialisation should be available in SàT return {f.var: unicode(f.value) for f in form.fields.values()} d.addCallback(serialize) return d def getConfiguration(self, client, service, nodeIdentifier): request = pubsub.PubSubRequest('configureGet') request.recipient = service request.nodeIdentifier = nodeIdentifier def cb(iq): form = data_form.findForm(iq.pubsub.configure, pubsub.NS_PUBSUB_NODE_CONFIG) form.typeCheck() return form d = request.send(client.xmlstream) d.addCallback(cb) return d def _setNodeConfiguration(self, service_s, nodeIdentifier, options, profile_key): client = self.host.getClient(profile_key) d = self.setConfiguration(client, jid.JID(service_s) if service_s else None, nodeIdentifier, options) return d def setConfiguration(self, client, service, nodeIdentifier, options): request = pubsub.PubSubRequest('configureSet') request.recipient = service request.nodeIdentifier = nodeIdentifier form = data_form.Form(formType='submit', formNamespace=pubsub.NS_PUBSUB_NODE_CONFIG) form.makeFields(options) request.options = form d = request.send(client.xmlstream) return d def _getAffiliations(self, service_s, nodeIdentifier, profile_key): client = self.host.getClient(profile_key) d = self.getAffiliations(client, jid.JID(service_s) if service_s else None, nodeIdentifier or None) return d def getAffiliations(self, client, service, nodeIdentifier=None): """Retrieve affiliations of an entity @param nodeIdentifier(unicode, None): node to get affiliation from None to get all nodes affiliations for this service """ request = pubsub.PubSubRequest('affiliations') request.recipient = service request.nodeIdentifier = nodeIdentifier def cb(iq_elt): try: affiliations_elt = next(iq_elt.pubsub.elements((pubsub.NS_PUBSUB, 'affiliations'))) except StopIteration: raise ValueError(_(u"Invalid result: missing element: {}").format(iq_elt.toXml)) try: return {e['node']: e['affiliation'] for e in affiliations_elt.elements((pubsub.NS_PUBSUB, 'affiliation'))} except KeyError: raise ValueError(_(u"Invalid result: bad element: {}").format(iq_elt.toXml)) d = request.send(client.xmlstream) d.addCallback(cb) return d def _getNodeAffiliations(self, service_s, nodeIdentifier, profile_key): client = self.host.getClient(profile_key) d = self.getNodeAffiliations(client, jid.JID(service_s) if service_s else None, nodeIdentifier) d.addCallback(lambda affiliations: {j.full(): a for j, a in affiliations.iteritems()}) return d def getNodeAffiliations(self, client, service, nodeIdentifier): """Retrieve affiliations of a node owned by profile""" request = pubsub.PubSubRequest('affiliationsGet') request.recipient = service request.nodeIdentifier = nodeIdentifier def cb(iq_elt): try: affiliations_elt = next(iq_elt.pubsub.elements((pubsub.NS_PUBSUB_OWNER, 'affiliations'))) except StopIteration: raise ValueError(_(u"Invalid result: missing element: {}").format(iq_elt.toXml)) try: return {jid.JID(e['jid']): e['affiliation'] for e in affiliations_elt.elements((pubsub.NS_PUBSUB_OWNER, 'affiliation'))} except KeyError: raise ValueError(_(u"Invalid result: bad element: {}").format(iq_elt.toXml)) d = request.send(client.xmlstream) d.addCallback(cb) return d def _setNodeAffiliations(self, service_s, nodeIdentifier, affiliations, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) affiliations = {jid.JID(jid_): affiliation for jid_, affiliation in affiliations.iteritems()} d = self.setNodeAffiliations(client, jid.JID(service_s) if service_s else None, nodeIdentifier, affiliations) return d def setNodeAffiliations(self, client, service, nodeIdentifier, affiliations): """Update affiliations of a node owned by profile @param affiliations(dict[jid.JID, unicode]): affiliations to set check https://xmpp.org/extensions/xep-0060.html#affiliations for a list of possible affiliations """ request = pubsub.PubSubRequest('affiliationsSet') request.recipient = service request.nodeIdentifier = nodeIdentifier request.affiliations = affiliations d = request.send(client.xmlstream) return d def _deleteNode(self, service_s, nodeIdentifier, profile_key): client = self.host.getClient(profile_key) return self.deleteNode(client, jid.JID(service_s) if service_s else None, nodeIdentifier) def deleteNode(self, client, service, nodeIdentifier): return client.pubsub_client.deleteNode(service, nodeIdentifier) def _addWatch(self, service_s, node, profile_key): """watch modifications on a node This method should only be called from bridge """ client = self.host.getClient(profile_key) service = jid.JID(service_s) if service_s else client.jid.userhostJID() client.pubsub_watching.add((service, node)) def _removeWatch(self, service_s, node, profile_key): """remove a node watch This method should only be called from bridge """ client = self.host.getClient(profile_key) service = jid.JID(service_s) if service_s else client.jid.userhostJID() client.pubsub_watching.remove((service, node)) def _retractItem(self, service_s, nodeIdentifier, itemIdentifier, notify, profile_key): return self._retractItems(service_s, nodeIdentifier, (itemIdentifier,), notify, profile_key) def _retractItems(self, service_s, nodeIdentifier, itemIdentifiers, notify, profile_key): return self.retractItems(jid.JID(service_s) if service_s else None, nodeIdentifier, itemIdentifiers, notify, profile_key) def retractItems(self, service, nodeIdentifier, itemIdentifiers, notify=True, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) return client.pubsub_client.retractItems(service, nodeIdentifier, itemIdentifiers, notify=True) def _subscribe(self, service, nodeIdentifier, options, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) service = None if not service else jid.JID(service) d = self.subscribe(client, service, nodeIdentifier, options=options or None) d.addCallback(lambda subscription: subscription.subscriptionIdentifier or u'') return d def subscribe(self, client, service, nodeIdentifier, sub_jid=None, options=None): # TODO: reimplement a subscribtion cache, checking that we have not subscription before trying to subscribe return client.pubsub_client.subscribe(service, nodeIdentifier, sub_jid or client.jid.userhostJID(), options=options) def _unsubscribe(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) service = None if not service else jid.JID(service) return self.unsubscribe(client, service, nodeIdentifier) def unsubscribe(self, client, service, nodeIdentifier, sub_jid=None, subscriptionIdentifier=None, sender=None): return client.pubsub_client.unsubscribe(service, nodeIdentifier, sub_jid or client.jid.userhostJID(), subscriptionIdentifier, sender) def _subscriptions(self, service, nodeIdentifier='', profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) service = None if not service else jid.JID(service) def gotSubscriptions(subscriptions): # we replace pubsub.Subscription instance by dict that we can serialize for idx, sub in enumerate(subscriptions): sub_dict = {'node': sub.nodeIdentifier, 'subscriber': sub.subscriber.full(), 'state': sub.state } if sub.subscriptionIdentifier is not None: sub_dict['id'] = sub.subscriptionIdentifier subscriptions[idx] = sub_dict return subscriptions d = self.subscriptions(client, service, nodeIdentifier or None) d.addCallback(gotSubscriptions) return d def subscriptions(self, client, service, nodeIdentifier=None): """retrieve subscriptions from a service @param service(jid.JID): PubSub service @param nodeIdentifier(unicode, None): node to check None to get all subscriptions """ return client.pubsub_client.subscriptions(service, nodeIdentifier) ## misc tools ## def getNodeURI(self, service, node, item=None): """Return XMPP URI of a PubSub node @param service(jid.JID): PubSub service @param node(unicode): node @return (unicode): URI of the node """ assert service is not None # XXX: urllib.urlencode use "&" to separate value, while XMPP URL (cf. RFC 5122) # use ";" as a separator. So if more than one value is used in query_data, # urlencode MUST NOT BE USED. query_data = [('node', node.encode('utf-8'))] if item is not None: query_data.append(('item', item.encode('utf-8'))) return "xmpp:{service}?;{query}".format( service=service.userhost(), query=urllib.urlencode(query_data) ).decode('utf-8') ## methods to manage several stanzas/jids at once ## # generic # def getRTResults(self, session_id, on_success=None, on_error=None, profile=C.PROF_KEY_NONE): return self.rt_sessions.getResults(session_id, on_success, on_error, profile) def serItemsData(self, items_data, item_cb=lambda item: item.toXml()): """Helper method to serialise result from [getItems] the items_data must be a tuple(list[domish.Element], dict[unicode, unicode]) as returned by [getItems]. metadata values are then casted to unicode and each item is passed to items_cb @param items_data(tuple): tuple returned by [getItems] @param item_cb(callable): method to transform each item @return (tuple): a serialised form ready to go throught bridge """ items, metadata = items_data return [item_cb(item) for item in items], {key: unicode(value) for key, value in metadata.iteritems()} def serItemsDataD(self, items_data, item_cb): """Helper method to serialise result from [getItems], deferred version the items_data must be a tuple(list[domish.Element], dict[unicode, unicode]) as returned by [getItems]. metadata values are then casted to unicode and each item is passed to items_cb An errback is added to item_cb, and when it is fired the value is filtered from final items @param items_data(tuple): tuple returned by [getItems] @param item_cb(callable): method to transform each item (must return a deferred) @return (tuple): a deferred which fire a serialised form ready to go throught bridge """ items, metadata = items_data def eb(failure): log.warning("Error while serialising/parsing item: {}".format(unicode(failure.value))) d = defer.gatherResults([item_cb(item).addErrback(eb) for item in items]) def finishSerialisation(serialised_items): return [item for item in serialised_items if item is not None], {key: unicode(value) for key, value in metadata.iteritems()} d.addCallback(finishSerialisation) return d def serDList(self, results, failure_result=None): """Serialise a DeferredList result @param results: DeferredList results @param failure_result: value to use as value for failed Deferred (default: empty tuple) @return (list): list with: - failure: empty in case of success, else error message - result """ if failure_result is None: failure_result = () return [('', result) if success else (unicode(result.result) or UNSPECIFIED, failure_result) for success, result in results] # subscribe # def _getNodeSubscriptions(self, service_s, nodeIdentifier, profile_key): client = self.host.getClient(profile_key) d = self.getNodeSubscriptions(client, jid.JID(service_s) if service_s else None, nodeIdentifier) d.addCallback(lambda subscriptions: {j.full(): a for j, a in subscriptions.iteritems()}) return d def getNodeSubscriptions(self, client, service, nodeIdentifier): """Retrieve subscriptions to a node @param nodeIdentifier(unicode): node to get subscriptions from """ if not nodeIdentifier: raise exceptions.DataError("node identifier can't be empty") request = pubsub.PubSubRequest('subscriptionsGet') request.recipient = service request.nodeIdentifier = nodeIdentifier def cb(iq_elt): try: subscriptions_elt = next(iq_elt.pubsub.elements((pubsub.NS_PUBSUB, 'subscriptions'))) except StopIteration: raise ValueError(_(u"Invalid result: missing element: {}").format(iq_elt.toXml)) except AttributeError as e: raise ValueError(_(u"Invalid result: {}").format(e)) try: return {jid.JID(s['jid']): s['subscription'] for s in subscriptions_elt.elements((pubsub.NS_PUBSUB, 'subscription'))} except KeyError: raise ValueError(_(u"Invalid result: bad element: {}").format(iq_elt.toXml)) d = request.send(client.xmlstream) d.addCallback(cb) return d def _setNodeSubscriptions(self, service_s, nodeIdentifier, subscriptions, profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) subscriptions = {jid.JID(jid_): subscription for jid_, subscription in subscriptions.iteritems()} d = self.setNodeSubscriptions(client, jid.JID(service_s) if service_s else None, nodeIdentifier, subscriptions) return d def setNodeSubscriptions(self, client, service, nodeIdentifier, subscriptions): """Set or update subscriptions of a node owned by profile @param subscriptions(dict[jid.JID, unicode]): subscriptions to set check https://xmpp.org/extensions/xep-0060.html#substates for a list of possible subscriptions """ request = pubsub.PubSubRequest('subscriptionsSet') request.recipient = service request.nodeIdentifier = nodeIdentifier request.subscriptions = {pubsub.Subscription(nodeIdentifier, jid_, state) for jid_, state in subscriptions.iteritems()} d = request.send(client.xmlstream) return d def _manySubscribeRTResult(self, session_id, profile_key=C.PROF_KEY_DEFAULT): """Get real-time results for subcribeToManu session @param session_id: id of the real-time deferred session @param return (tuple): (remaining, results) where: - remaining is the number of still expected results - results is a list of tuple(unicode, unicode, bool, unicode) with: - service: pubsub service - and node: pubsub node - failure(unicode): empty string in case of success, error message else @param profile_key: %(doc_profile_key)s """ profile = self.host.getClient(profile_key).profile d = self.rt_sessions.getResults(session_id, on_success=lambda result:'', on_error=lambda failure:unicode(failure.value), profile=profile) # we need to convert jid.JID to unicode with full() to serialise it for the bridge d.addCallback(lambda ret: (ret[0], [(service.full(), node, '' if success else failure or UNSPECIFIED) for (service, node), (success, failure) in ret[1].iteritems()])) return d def _subscribeToMany(self, node_data, subscriber=None, options=None, profile_key=C.PROF_KEY_NONE): return self.subscribeToMany([(jid.JID(service), unicode(node)) for service, node in node_data], jid.JID(subscriber), options, profile_key) def subscribeToMany(self, node_data, subscriber, options=None, profile_key=C.PROF_KEY_NONE): """Subscribe to several nodes at once. @param node_data (iterable[tuple]): iterable of tuple (service, node) where: - service (jid.JID) is the pubsub service - node (unicode) is the node to subscribe to @param subscriber (jid.JID): optional subscription identifier. @param options (dict): subscription options @param profile_key (str): %(doc_profile_key)s @return (str): RT Deferred session id """ client = self.host.getClient(profile_key) deferreds = {} for service, node in node_data: deferreds[(service, node)] = client.pubsub_client.subscribe(service, node, subscriber, options=options) return self.rt_sessions.newSession(deferreds, client.profile) # found_nodes = yield self.listNodes(service, profile=client.profile) # subscribed_nodes = yield self.listSubscribedNodes(service, profile=client.profile) # d_list = [] # for nodeIdentifier in (set(nodeIdentifiers) - set(subscribed_nodes)): # if nodeIdentifier not in found_nodes: # log.debug(u"Skip the subscription to [{node}]: node doesn't exist".format(node=nodeIdentifier)) # continue # avoid sat-pubsub "SubscriptionExists" error # d_list.append(client.pubsub_client.subscribe(service, nodeIdentifier, sub_jid or client.pubsub_client.parent.jid.userhostJID(), options=options)) # defer.returnValue(d_list) # get # def _getFromManyRTResult(self, session_id, profile_key=C.PROF_KEY_DEFAULT): """Get real-time results for getFromMany session @param session_id: id of the real-time deferred session @param profile_key: %(doc_profile_key)s @param return (tuple): (remaining, results) where: - remaining is the number of still expected results - results is a list of tuple with - service (unicode): pubsub service - node (unicode): pubsub node - failure (unicode): empty string in case of success, error message else - items (list[s]): raw XML of items - metadata(dict): serialised metadata """ profile = self.host.getClient(profile_key).profile d = self.rt_sessions.getResults(session_id, on_success=lambda result: ('', self.serItemsData(result)), on_error=lambda failure: (unicode(failure.value) or UNSPECIFIED, ([],{})), profile=profile) d.addCallback(lambda ret: (ret[0], [(service.full(), node, failure, items, metadata) for (service, node), (success, (failure, (items, metadata))) in ret[1].iteritems()])) return d def _getFromMany(self, node_data, max_item=10, extra_dict=None, profile_key=C.PROF_KEY_NONE): """ @param max_item(int): maximum number of item to get, C.NO_LIMIT for no limit """ max_item = None if max_item == C.NO_LIMIT else max_item extra = self.parseExtra(extra_dict) return self.getFromMany([(jid.JID(service), unicode(node)) for service, node in node_data], max_item, extra.rsm_request, extra.extra, profile_key) def getFromMany(self, node_data, max_item=None, rsm_request=None, extra=None, profile_key=C.PROF_KEY_NONE): """Get items from many nodes at once @param node_data (iterable[tuple]): iterable of tuple (service, node) where: - service (jid.JID) is the pubsub service - node (unicode) is the node to get items from @param max_items (int): optional limit on the number of retrieved items. @param rsm_request (RSMRequest): RSM request data @param profile_key (unicode): %(doc_profile_key)s @return (str): RT Deferred session id """ client = self.host.getClient(profile_key) deferreds = {} for service, node in node_data: deferreds[(service, node)] = self.getItems(client, service, node, max_item, rsm_request=rsm_request, extra=extra) return self.rt_sessions.newSession(deferreds, client.profile) class SatPubSubClient(rsm.PubSubClient): implements(disco.IDisco) def __init__(self, host, parent_plugin): self.host = host self.parent_plugin = parent_plugin rsm.PubSubClient.__init__(self) def connectionInitialized(self): rsm.PubSubClient.connectionInitialized(self) def _getNodeCallbacks(self, node, event): """Generate callbacks from given node and event @param node(unicode): node used for the item any registered node which prefix the node will match @param event(unicode): one of C.PS_ITEMS, C.PS_RETRACT, C.PS_DELETE @return (iterator[callable]): callbacks for this node/event """ for registered_node, callbacks_dict in self.parent_plugin._node_cb.iteritems(): if not node.startswith(registered_node): continue try: for callback in callbacks_dict[event]: yield callback except KeyError: continue def itemsReceived(self, event): log.debug(u"Pubsub items received") for callback in self._getNodeCallbacks(event.nodeIdentifier, C.PS_ITEMS): callback(self.parent, event) client = self.parent if (event.sender, event.nodeIdentifier) in client.pubsub_watching: raw_items = [i.toXml() for i in event.items] self.host.bridge.psEventRaw(event.sender.full(), event.nodeIdentifier, C.PS_ITEMS, raw_items, client.profile) def deleteReceived(self, event): log.debug((u"Publish node deleted")) for callback in self._getNodeCallbacks(event.nodeIdentifier, C.PS_DELETE): callback(self.parent, event) client = self.parent if (event.sender, event.nodeIdentifier) in client.pubsub_watching: self.host.bridge.psEventRaw(event.sender.full(), event.nodeIdentifier, C.PS_DELETE, [], client.profile) def subscriptions(self, service, nodeIdentifier, sender=None): """Return the list of subscriptions to the given service and node. @param service: The publish subscribe service to retrieve the subscriptions from. @type service: L{JID} @param nodeIdentifier: The identifier of the node (leave empty to retrieve all subscriptions). @type nodeIdentifier: C{unicode} @return (list[pubsub.Subscription]): list of subscriptions """ request = pubsub.PubSubRequest('subscriptions') request.recipient = service request.nodeIdentifier = nodeIdentifier request.sender = sender d = request.send(self.xmlstream) def cb(iq): subs = [] for subscription_elt in iq.pubsub.subscriptions.elements(pubsub.NS_PUBSUB, 'subscription'): subscription = pubsub.Subscription(subscription_elt['node'], jid.JID(subscription_elt['jid']), subscription_elt['subscription'], subscriptionIdentifier=subscription_elt.getAttribute('subid')) subs.append(subscription) return subs return d.addCallback(cb) def getDiscoInfo(self, requestor, service, nodeIdentifier=''): disco_info = [] self.host.trigger.point("PubSub Disco Info", disco_info, self.parent.profile) return disco_info def getDiscoItems(self, requestor, service, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0231.py0000644000175500017600000001175013243470025021440 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Jingle File Transfer (XEP-0231) # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.tools import xml_tools from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.xmlstream import XMPPHandler import base64 PLUGIN_INFO = { C.PI_NAME: "Bits of Binary", C.PI_IMPORT_NAME: "XEP-0231", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0231"], C.PI_DEPENDENCIES: ["XEP-0071"], C.PI_MAIN: "XEP_0231", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of bits of binary (used for small images/files)""") } NS_BOB = u'urn:xmpp:bob' class XEP_0231(object): def __init__(self, host): log.info(_(u"plugin Bits of Binary initialization")) self.host = host host.trigger.add("xhtml_post_treat", self.XHTMLTrigger) def dumpData(self, client, data_elt, cid): """save file encoded in data_elt to cache @param data_elt(domish.Element): as in XEP-0231 @param cid(unicode): content-id @return(unicode): full path to dumped file """ # FIXME: is it needed to use a separate thread? # probably not with the little data expected with BoB mime_type = data_elt.getAttribute('type','') try: max_age = int(data_elt['max-age']) except (KeyError, ValueError): log.warning(u'invalid max-age found') max_age = None with client.cache.cacheData( PLUGIN_INFO['import_name'], cid, mime_type, max_age) as f: file_path = f.name f.write(base64.b64decode(str(data_elt))) return file_path def getHandler(self, client): return XEP_0231_handler() def _dataCb(self, iq_elt, client, img_elt, cid): for data_elt in iq_elt.elements(NS_BOB, u'data'): if data_elt.getAttribute('cid') == cid: file_path = self.dumpData(client, data_elt, cid) img_elt[u'src'] = u'file://{}'.format(file_path) break else: log.warning(u"invalid data stanza received, requested cid was not found:\n{iq_elt}\nrequested cid: {cid}".format( iq_elt = iq_elt, cid = cid )) def _dataEb(self, iq_elt): log.warning(u"Can't get requested data:\n{iq_elt}".format(iq_elt=iq_elt)) def XHTMLTrigger(self, client, message_elt, body_elt, lang, treat_d): for img_elt in xml_tools.findAll(body_elt, C.NS_XHTML, u'img'): source = img_elt.getAttribute(u'src','') if source.startswith(u'cid:'): cid = source[4:] file_path = client.cache.getFilePath(cid) if file_path is not None: # image is in cache, we change change the url img_elt[u'src'] = u'file://{}'.format(file_path) continue else: # image is not in cache, is it given locally? for data_elt in message_elt.elements(NS_BOB, u'data'): if data_elt.getAttribute('cid') == cid: file_path = self.dumpData(data_elt, cid) img_elt[u'src'] = u'file://{}'.format(file_path) break else: # cid not found locally, we need to request it # so we use the deferred iq_elt = client.IQ('get') iq_elt['to'] = message_elt['from'] data_elt = iq_elt.addElement((NS_BOB, 'data')) data_elt['cid'] = cid d = iq_elt.send() d.addCallback(self._dataCb, client, img_elt, cid) d.addErrback(self._dataEb) treat_d.addCallback(lambda dummy: d) class XEP_0231_handler(XMPPHandler): implements(iwokkel.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_BOB)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0184.py0000644000175500017600000001575313243470025021456 0ustar debaclelocal_src#!/usr/bin/python # -*- coding: utf-8 -*- # SAT plugin for managing xep-0184 # Copyright (C) 2009-2016 Geoffrey POUZET (chteufleur@kingpenguin.tk) # 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger from twisted.internet import reactor from twisted.words.protocols.jabber import xmlstream, jid from twisted.words.xish import domish log = getLogger(__name__) from wokkel import disco, iwokkel from zope.interface import implements try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: from wokkel.subprotocols import XMPPHandler NS_MESSAGE_DELIVERY_RECEIPTS = 'urn:xmpp:receipts' MSG = 'message' MSG_CHAT = '/'+MSG+'[@type="chat"]' MSG_CHAT_MESSAGE_DELIVERY_RECEIPTS_REQUEST = MSG_CHAT+'/request[@xmlns="'+NS_MESSAGE_DELIVERY_RECEIPTS+'"]' MSG_CHAT_MESSAGE_DELIVERY_RECEIPTS_RECEIVED = MSG_CHAT+'/received[@xmlns="'+NS_MESSAGE_DELIVERY_RECEIPTS+'"]' MSG_NORMAL = '/'+MSG+'[@type="normal"]' MSG_NORMAL_MESSAGE_DELIVERY_RECEIPTS_REQUEST = MSG_NORMAL+'/request[@xmlns="'+NS_MESSAGE_DELIVERY_RECEIPTS+'"]' MSG_NORMAL_MESSAGE_DELIVERY_RECEIPTS_RECEIVED = MSG_NORMAL+'/received[@xmlns="'+NS_MESSAGE_DELIVERY_RECEIPTS+'"]' PARAM_KEY = "Privacy" PARAM_NAME = "Enable message delivery receipts" ENTITY_KEY = PARAM_KEY + "_" + PARAM_NAME PLUGIN_INFO = { C.PI_NAME: "XEP-0184 Plugin", C.PI_IMPORT_NAME: "XEP-0184", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0184"], C.PI_DEPENDENCIES: [], C.PI_MAIN: "XEP_0184", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Message Delivery Receipts""") } STATUS_MESSAGE_DELIVERY_RECEIVED = "delivered" TEMPO_DELETE_WAITING_ACK_S = 300 # 5 min class XEP_0184(object): """ Implementation for XEP 0184. """ params = """ """ % { 'category_name': PARAM_KEY, 'category_label': _(PARAM_KEY), 'param_name': PARAM_NAME, 'param_label': _('Enable message delivery receipts') } def __init__(self, host): log.info(_("Plugin XEP_0184 (message delivery receipts) initialization")) self.host = host self._dictRequest = dict() # parameter value is retrieved before each use host.memory.updateParams(self.params) host.trigger.add("sendMessage", self.sendMessageTrigger) host.bridge.addSignal("messageState", ".plugin", signature='sss') # message_uid, status, profile def getHandler(self, client): return XEP_0184_handler(self, client.profile) def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): """Install SendMessage command hook """ def treatment(mess_data): message = mess_data['xml'] message_type = message.getAttribute("type") if self._isActif(client.profile) and (message_type == "chat" or message_type == "normal"): message.addElement('request', NS_MESSAGE_DELIVERY_RECEIPTS) uid = mess_data['uid'] msg_id = message.getAttribute("id") self._dictRequest[msg_id] = uid reactor.callLater(TEMPO_DELETE_WAITING_ACK_S, self._clearDictRequest, msg_id) log.debug(_("[XEP-0184] Request acknowledgment for message id {}".format(msg_id))) return mess_data post_xml_treatments.addCallback(treatment) return True def onMessageDeliveryReceiptsRequest(self, msg_elt, client): """This method is called on message delivery receipts **request** (XEP-0184 #7) @param msg_elt: message element @param client: %(doc_client)s""" from_jid = jid.JID(msg_elt['from']) if self._isActif(client.profile) and client.roster.isPresenceAuthorised(from_jid): received_elt_ret = domish.Element((NS_MESSAGE_DELIVERY_RECEIPTS, 'received')) received_elt_ret["id"] = msg_elt["id"] msg_result_elt = xmlstream.toResponse(msg_elt, 'result') msg_result_elt.addChild(received_elt_ret) client.send(msg_result_elt) def onMessageDeliveryReceiptsReceived(self, msg_elt, client): """This method is called on message delivery receipts **received** (XEP-0184 #7) @param msg_elt: message element @param client: %(doc_client)s""" msg_elt.handled = True rcv_elt = msg_elt.elements(NS_MESSAGE_DELIVERY_RECEIPTS, 'received').next() msg_id = rcv_elt['id'] try: uid = self._dictRequest[msg_id] del self._dictRequest[msg_id] self.host.bridge.messageState(uid, STATUS_MESSAGE_DELIVERY_RECEIVED, client.profile) log.debug(_("[XEP-0184] Receive acknowledgment for message id {}".format(msg_id))) except KeyError: pass def _clearDictRequest(self, msg_id): try: del self._dictRequest[msg_id] log.debug(_("[XEP-0184] Delete waiting acknowledgment for message id {}".format(msg_id))) except KeyError: pass def _isActif(self, profile): return self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile) class XEP_0184_handler(XMPPHandler): implements(iwokkel.IDisco) def __init__(self, plugin_parent, profile): self.plugin_parent = plugin_parent self.host = plugin_parent.host self.profile = profile def connectionInitialized(self): self.xmlstream.addObserver(MSG_CHAT_MESSAGE_DELIVERY_RECEIPTS_REQUEST, self.plugin_parent.onMessageDeliveryReceiptsRequest, client=self.parent) self.xmlstream.addObserver(MSG_CHAT_MESSAGE_DELIVERY_RECEIPTS_RECEIVED, self.plugin_parent.onMessageDeliveryReceiptsReceived, client=self.parent) self.xmlstream.addObserver(MSG_NORMAL_MESSAGE_DELIVERY_RECEIPTS_REQUEST, self.plugin_parent.onMessageDeliveryReceiptsRequest, client=self.parent) self.xmlstream.addObserver(MSG_NORMAL_MESSAGE_DELIVERY_RECEIPTS_RECEIVED, self.plugin_parent.onMessageDeliveryReceiptsReceived, client=self.parent) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_MESSAGE_DELIVERY_RECEIPTS)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0363.py0000644000175500017600000003040213243470025021441 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Jingle File Transfer (XEP-0363) # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.internet import reactor from twisted.internet import defer from twisted.internet import ssl from twisted.internet.interfaces import IOpenSSLClientConnectionCreator from twisted.web import client as http_client from twisted.web import http_headers from twisted.web import iweb from twisted.python import failure from collections import namedtuple from zope.interface import implementer from OpenSSL import SSL import os.path import mimetypes PLUGIN_INFO = { C.PI_NAME: "HTTP File Upload", C.PI_IMPORT_NAME: "XEP-0363", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0363"], C.PI_DEPENDENCIES: ["FILE", "UPLOAD"], C.PI_MAIN: "XEP_0363", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of HTTP File Upload""") } NS_HTTP_UPLOAD = 'urn:xmpp:http:upload' Slot = namedtuple('Slot', ['put', 'get']) @implementer(IOpenSSLClientConnectionCreator) class NoCheckConnectionCreator(object): def __init__(self, hostname, ctx): self._ctx = ctx def clientConnectionForTLS(self, tlsProtocol): context = self._ctx connection = SSL.Connection(context, None) connection.set_app_data(tlsProtocol) return connection @implementer(iweb.IPolicyForHTTPS) class NoCheckContextFactory(ssl.ClientContextFactory): """Context factory which doesn't do TLS certificate check /!\\ it's obvisously a security flaw to use this class, and it should be used only wiht explicite agreement from the end used """ def creatorForNetloc(self, hostname, port): log.warning(u"TLS check disabled for {host} on port {port}".format(host=hostname, port=port)) certificateOptions = ssl.CertificateOptions(trustRoot=None) return NoCheckConnectionCreator(hostname, certificateOptions.getContext()) class XEP_0363(object): def __init__(self, host): log.info(_("plugin HTTP File Upload initialization")) self.host = host host.bridge.addMethod("fileHTTPUpload", ".plugin", in_sign='sssbs', out_sign='', method=self._fileHTTPUpload) host.bridge.addMethod("fileHTTPUploadGetSlot", ".plugin", in_sign='sisss', out_sign='(ss)', method=self._getSlot, async=True) host.plugins['UPLOAD'].register(u"HTTP Upload", self.getHTTPUploadEntity, self.fileHTTPUpload) def getHandler(self, client): return XEP_0363_handler() @defer.inlineCallbacks def getHTTPUploadEntity(self, upload_jid=None, profile=C.PROF_KEY_NONE): """Get HTTP upload capable entity upload_jid is checked, then its components @param upload_jid(None, jid.JID): entity to check @return(D(jid.JID)): first HTTP upload capable entity @raise exceptions.NotFound: no entity found """ client = self.host.getClient(profile) try: entity = client.http_upload_service except AttributeError: found_entities = yield self.host.findFeaturesSet(client, (NS_HTTP_UPLOAD,)) try: entity = client.http_upload_service = iter(found_entities).next() except StopIteration: entity = client.http_upload_service = None if entity is None: raise failure.Failure(exceptions.NotFound(u'No HTTP upload entity found')) defer.returnValue(entity) def _fileHTTPUpload(self, filepath, filename='', upload_jid='', ignore_tls_errors=False, profile=C.PROF_KEY_NONE): assert os.path.isabs(filepath) and os.path.isfile(filepath) progress_id_d, dummy = self.fileHTTPUpload(filepath, filename or None, jid.JID(upload_jid) if upload_jid else None, {'ignore_tls_errors': ignore_tls_errors}, profile) return progress_id_d def fileHTTPUpload(self, filepath, filename=None, upload_jid=None, options=None, profile=C.PROF_KEY_NONE): """upload a file through HTTP @param filepath(str): absolute path of the file @param filename(None, unicode): name to use for the upload None to use basename of the path @param upload_jid(jid.JID, None): upload capable entity jid, or None to use autodetected, if possible @param options(dict): options where key can be: - ignore_tls_errors(bool): if True, SSL certificate will not be checked @param profile: %(doc_profile)s @return (D(tuple[D(unicode), D(unicode)])): progress id and Deferred which fire download URL """ if options is None: options = {} ignore_tls_errors = options.get('ignore_tls_errors', False) client = self.host.getClient(profile) filename = filename or os.path.basename(filepath) size = os.path.getsize(filepath) progress_id_d = defer.Deferred() download_d = defer.Deferred() d = self.getSlot(client, filename, size, upload_jid=upload_jid) d.addCallbacks(self._getSlotCb, self._getSlotEb, (client, progress_id_d, download_d, filepath, size, ignore_tls_errors), None, (client, progress_id_d, download_d)) return progress_id_d, download_d def _getSlotEb(self, fail, client, progress_id_d, download_d): """an error happened while trying to get slot""" log.warning(u"Can't get upload slot: {reason}".format(reason=fail.value)) progress_id_d.errback(fail) download_d.errback(fail) def _getSlotCb(self, slot, client, progress_id_d, download_d, path, size, ignore_tls_errors=False): """Called when slot is received, try to do the upload @param slot(Slot): slot instance with the get and put urls @param progress_id_d(defer.Deferred): Deferred to call when progress_id is known @param progress_id_d(defer.Deferred): Deferred to call with URL when upload is done @param path(str): path to the file to upload @param size(int): size of the file to upload @param ignore_tls_errors(bool): ignore TLS certificate is True @return (tuple """ log.debug(u"Got upload slot: {}".format(slot)) sat_file = self.host.plugins['FILE'].File(self.host, client, path, size=size, auto_end_signals=False) progress_id_d.callback(sat_file.uid) file_producer = http_client.FileBodyProducer(sat_file) if ignore_tls_errors: agent = http_client.Agent(reactor, NoCheckContextFactory()) else: agent = http_client.Agent(reactor) d = agent.request('PUT', slot.put.encode('utf-8'), http_headers.Headers({'User-Agent': [C.APP_NAME.encode('utf-8')]}), file_producer) d.addCallbacks(self._uploadCb, self._uploadEb, (sat_file, slot, download_d), None, (sat_file, download_d)) return d def _uploadCb(self, dummy, sat_file, slot, download_d): """Called once file is successfully uploaded @param sat_file(SatFile): file used for the upload should be closed, be is needed to send the progressFinished signal @param slot(Slot): put/get urls """ log.info(u"HTTP upload finished") sat_file.progressFinished({'url': slot.get}) download_d.callback(slot.get) def _uploadEb(self, fail, sat_file, download_d): """Called on unsuccessful upload @param sat_file(SatFile): file used for the upload should be closed, be is needed to send the progressError signal """ download_d.errback(fail) try: wrapped_fail = fail.value.reasons[0] except (AttributeError, IndexError): sat_file.progressError(unicode(fail)) raise fail else: if wrapped_fail.check(SSL.Error): msg = u"TLS validation error, can't connect to HTTPS server" log.warning(msg + ": " + unicode(wrapped_fail.value)) sat_file.progressError(msg) def _gotSlot(self, iq_elt, client): """Slot have been received This method convert the iq_elt result to a Slot instance @param iq_elt(domish.Element): result as specified in XEP-0363 """ try: slot_elt = iq_elt.elements(NS_HTTP_UPLOAD, 'slot').next() put_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'put').next()) get_url = unicode(slot_elt.elements(NS_HTTP_UPLOAD, 'get').next()) except StopIteration: raise exceptions.DataError(u"Incorrect stanza received from server") slot = Slot(put=put_url, get=get_url) return slot def _getSlot(self, filename, size, content_type, upload_jid, profile_key=C.PROF_KEY_NONE): """Get a upload slot This method can be used when uploading is done by the frontend @param filename(unicode): name of the file to upload @param size(int): size of the file (must be non null) @param upload_jid(jid.JID(), None, ''): HTTP upload capable entity @param content_type(unicode, None): MIME type of the content empty string or None to guess automatically """ filename = filename.replace('/', '_') client = self.host.getClient(profile_key) return self.getSlot(client, filename, size, content_type or None, upload_jid or None) def getSlot(self, client, filename, size, content_type=None, upload_jid=None): """Get a slot (i.e. download/upload links) @param filename(unicode): name to use for the upload @param size(int): size of the file to upload (must be >0) @param content_type(None, unicode): MIME type of the content None to autodetect @param upload_jid(jid.JID, None): HTTP upload capable upload_jid or None to use the server component (if any) @param client: %(doc_client)s @return (Slot): the upload (put) and download (get) URLs @raise exceptions.NotFound: no HTTP upload capable upload_jid has been found """ assert filename and size if content_type is None: # TODO: manage python magic for file guessing (in a dedicated plugin ?) content_type = mimetypes.guess_type(filename, strict=False)[0] if upload_jid is None: try: upload_jid = client.http_upload_service except AttributeError: d = self.getHTTPUploadEntity(profile=client.profile) d.addCallback(lambda found_entity: self.getSlot(client, filename, size, content_type, found_entity)) return d else: if upload_jid is None: raise failure.Failure(exceptions.NotFound(u'No HTTP upload entity found')) iq_elt = client.IQ('get') iq_elt['to'] = upload_jid.full() request_elt = iq_elt.addElement((NS_HTTP_UPLOAD, 'request')) request_elt.addElement('filename', content=filename) request_elt.addElement('size', content=unicode(size)) if content_type is not None: request_elt.addElement('content-type', content=content_type) d = iq_elt.send() d.addCallback(self._gotSlot, client) return d class XEP_0363_handler(XMPPHandler): implements(iwokkel.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_HTTP_UPLOAD)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0085.py0000755000175500017600000004042313243470025021451 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Chat State Notifications Protocol (xep-0085) # Copyright (C) 2009-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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core import exceptions from sat.core.log import getLogger log = getLogger(__name__) from wokkel import disco, iwokkel from zope.interface import implements from twisted.words.protocols.jabber.jid import JID try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: from wokkel.subprotocols import XMPPHandler from twisted.words.xish import domish from twisted.internet import reactor from twisted.internet import error as internet_error NS_XMPP_CLIENT = "jabber:client" NS_CHAT_STATES = "http://jabber.org/protocol/chatstates" CHAT_STATES = ["active", "inactive", "gone", "composing", "paused"] MESSAGE_TYPES = ["chat", "groupchat"] PARAM_KEY = "Notifications" PARAM_NAME = "Enable chat state notifications" ENTITY_KEY = PARAM_KEY + "_" + PARAM_NAME DELETE_VALUE = "DELETE" PLUGIN_INFO = { C.PI_NAME: "Chat State Notifications Protocol Plugin", C.PI_IMPORT_NAME: "XEP-0085", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0085"], C.PI_DEPENDENCIES: [], C.PI_MAIN: "XEP_0085", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Chat State Notifications Protocol""") } # Describe the internal transitions that are triggered # by a timer. Beside that, external transitions can be # runned to target the states "active" or "composing". # Delay is specified here in seconds. TRANSITIONS = { "active": {"next_state": "inactive", "delay": 120}, "inactive": {"next_state": "gone", "delay": 480}, "gone": {"next_state": "", "delay": 0}, "composing": {"next_state": "paused", "delay": 30}, "paused": {"next_state": "inactive", "delay": 450} } class UnknownChatStateException(Exception): """ This error is raised when an unknown chat state is used. """ pass class XEP_0085(object): """ Implementation for XEP 0085 """ params = """ """ % { 'category_name': PARAM_KEY, 'category_label': _(PARAM_KEY), 'param_name': PARAM_NAME, 'param_label': _('Enable chat state notifications') } def __init__(self, host): log.info(_("Chat State Notifications plugin initialization")) self.host = host self.map = {} # FIXME: would be better to use client instead of mapping profile to data # parameter value is retrieved before each use host.memory.updateParams(self.params) # triggers from core host.trigger.add("MessageReceived", self.messageReceivedTrigger) host.trigger.add("sendMessage", self.sendMessageTrigger) host.trigger.add("paramUpdateTrigger", self.paramUpdateTrigger) # args: to_s (jid as string), profile host.bridge.addMethod("chatStateComposing", ".plugin", in_sign='ss', out_sign='', method=self.chatStateComposing) # args: from (jid as string), state in CHAT_STATES, profile host.bridge.addSignal("chatStateReceived", ".plugin", signature='sss') def getHandler(self, client): return XEP_0085_handler(self, client.profile) def profileDisconnected(self, client): """Eventually send a 'gone' state to all one2one contacts.""" profile = client.profile if profile not in self.map: return for to_jid in self.map[profile]: # FIXME: the "unavailable" presence stanza is received by to_jid # before the chat state, so it will be ignored... find a way to # actually defer the disconnection self.map[profile][to_jid]._onEvent('gone') del self.map[profile] def updateCache(self, entity_jid, value, profile): """Update the entity data of the given profile for one or all contacts. Reset the chat state(s) display if the notification has been disabled. @param entity_jid: contact's JID, or C.ENTITY_ALL to update all contacts. @param value: True, False or DELETE_VALUE to delete the entity data @param profile: current profile """ if value == DELETE_VALUE: self.host.memory.delEntityDatum(entity_jid, ENTITY_KEY, profile) else: self.host.memory.updateEntityData(entity_jid, ENTITY_KEY, value, profile_key=profile) if not value or value == DELETE_VALUE: # reinit chat state UI for this or these contact(s) self.host.bridge.chatStateReceived(entity_jid.full(), "", profile) def paramUpdateTrigger(self, name, value, category, type_, profile): """Reset all the existing chat state entity data associated with this profile after a parameter modification. @param name: parameter name @param value: "true" to activate the notifications, or any other value to delete it @param category: parameter category @param type_: parameter type """ if (category, name) == (PARAM_KEY, PARAM_NAME): self.updateCache(C.ENTITY_ALL, True if C.bool(value) else DELETE_VALUE, profile=profile) return False return True def messageReceivedTrigger(self, client, message, post_treat): """ Update the entity cache when we receive a message with body. Check for a chat state in the message and signal frontends. """ profile = client.profile if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return True from_jid = JID(message.getAttribute("from")) if self._isMUC(from_jid, profile): from_jid = from_jid.userhostJID() else: # update entity data for one2one chat # assert from_jid.resource # FIXME: assert doesn't work on normal message from server (e.g. server announce), because there is no resource try: domish.generateElementsNamed(message.elements(), name="body").next() try: domish.generateElementsNamed(message.elements(), name="active").next() # contact enabled Chat State Notifications self.updateCache(from_jid, True, profile=profile) except StopIteration: if message.getAttribute('type') == 'chat': # contact didn't enable Chat State Notifications self.updateCache(from_jid, False, profile=profile) return True except StopIteration: pass # send our next "composing" states to any MUC and to the contacts who enabled the feature self._chatStateInit(from_jid, message.getAttribute("type"), profile) state_list = [child.name for child in message.elements() if message.getAttribute("type") in MESSAGE_TYPES and child.name in CHAT_STATES and child.defaultUri == NS_CHAT_STATES] for state in state_list: # there must be only one state according to the XEP if state != 'gone' or message.getAttribute('type') != 'groupchat': self.host.bridge.chatStateReceived(message.getAttribute("from"), state, profile) break return True def sendMessageTrigger(self, client, mess_data, pre_xml_treatments, post_xml_treatments): """ Eventually add the chat state to the message and initiate the state machine when sending an "active" state. """ profile = client.profile def treatment(mess_data): message = mess_data['xml'] to_jid = JID(message.getAttribute("to")) if not self._checkActivation(to_jid, forceEntityData=True, profile=profile): return mess_data try: # message with a body always mean active state domish.generateElementsNamed(message.elements(), name="body").next() message.addElement('active', NS_CHAT_STATES) # launch the chat state machine (init the timer) if self._isMUC(to_jid, profile): to_jid = to_jid.userhostJID() self._chatStateActive(to_jid, mess_data["type"], profile) except StopIteration: if "chat_state" in mess_data["extra"]: state = mess_data["extra"].pop("chat_state") assert state in CHAT_STATES message.addElement(state, NS_CHAT_STATES) return mess_data post_xml_treatments.addCallback(treatment) return True def _isMUC(self, to_jid, profile): """Tell if that JID is a MUC or not @param to_jid (JID): full or bare JID to check @param profile (str): %(doc_profile)s @return: bool """ try: type_ = self.host.memory.getEntityDatum(to_jid.userhostJID(), 'type', profile) if type_ == 'chatroom': # FIXME: should not use disco instead ? return True except (exceptions.UnknownEntityError, KeyError): pass return False def _checkActivation(self, to_jid, forceEntityData, profile): """ @param to_jid: the contact's full JID (or bare if you know it's a MUC) @param forceEntityData: if set to True, a non-existing entity data will be considered to be True (and initialized) @param: current profile @return: True if the notifications should be sent to this JID. """ # check if the parameter is active if not self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile): return False # check if notifications should be sent to this contact if self._isMUC(to_jid, profile): return True # FIXME: this assertion crash when we want to send a message to an online bare jid # assert to_jid.resource or not self.host.memory.isEntityAvailable(to_jid, profile) # must either have a resource, or talk to an offline contact try: return self.host.memory.getEntityDatum(to_jid, ENTITY_KEY, profile) except (exceptions.UnknownEntityError, KeyError): if forceEntityData: # enable it for the first time self.updateCache(to_jid, True, profile=profile) return True # wait for the first message before sending states return False def _chatStateInit(self, to_jid, mess_type, profile): """ Data initialization for the chat state machine. @param to_jid (JID): full JID for one2one, bare JID for MUC @param mess_type (str): "one2one" or "groupchat" @param profile (str): %(doc_profile)s """ if mess_type is None: return profile_map = self.map.setdefault(profile, {}) if to_jid not in profile_map: machine = ChatStateMachine(self.host, to_jid, mess_type, profile) self.map[profile][to_jid] = machine def _chatStateActive(self, to_jid, mess_type, profile_key): """ Launch the chat state machine on "active" state. @param to_jid (JID): full JID for one2one, bare JID for MUC @param mess_type (str): "one2one" or "groupchat" @param profile (str): %(doc_profile)s """ profile = self.host.memory.getProfileName(profile_key) if profile is None: raise exceptions.ProfileUnknownError self._chatStateInit(to_jid, mess_type, profile) self.map[profile][to_jid]._onEvent("active") def chatStateComposing(self, to_jid_s, profile_key): """Move to the "composing" state when required. Since this method is called from the front-end, it needs to check the values of the parameter "Send chat state notifications" and the entity data associated to the target JID. @param to_jid_s (str): contact full JID as a string @param profile_key (str): %(doc_profile_key)s """ # TODO: try to optimize this method which is called often client = self.host.getClient(profile_key) to_jid = JID(to_jid_s) if self._isMUC(to_jid, client.profile): to_jid = to_jid.userhostJID() elif not to_jid.resource: to_jid.resource = self.host.memory.getMainResource(client, to_jid) if not self._checkActivation(to_jid, forceEntityData=False, profile=client.profile): return try: self.map[client.profile][to_jid]._onEvent("composing") except (KeyError, AttributeError): # no message has been sent/received since the notifications # have been enabled, it's better to wait for a first one pass class ChatStateMachine(object): """ This class represents a chat state, between one profile and one target contact. A timer is used to move from one state to the other. The initialization is done through the "active" state which is internally set when a message is sent. The state "composing" can be set externally (through the bridge by a frontend). Other states are automatically set with the timer. """ def __init__(self, host, to_jid, mess_type, profile): """ Initialization need to store the target, message type and a profile for sending later messages. """ self.host = host self.to_jid = to_jid self.mess_type = mess_type self.profile = profile self.state = None self.timer = None def _onEvent(self, state): """ Move to the specified state, eventually send the notification to the contact (the "active" state is automatically sent with each message) and set the timer. """ assert state in TRANSITIONS transition = TRANSITIONS[state] assert "next_state" in transition and "delay" in transition if state != self.state and state != "active": if state != 'gone' or self.mess_type != 'groupchat': # send a new message without body log.debug(u"sending state '{state}' to {jid}".format(state=state, jid=self.to_jid.full())) client = self.host.getClient(self.profile) mess_data = { 'from': client.jid, 'to': self.to_jid, 'uid': '', 'message': {}, 'type': self.mess_type, 'subject': {}, 'extra': {}, } client.generateMessageXML(mess_data) mess_data['xml'].addElement(state, NS_CHAT_STATES) client.send(mess_data['xml']) self.state = state try: self.timer.cancel() except (internet_error.AlreadyCalled, AttributeError): pass if transition["next_state"] and transition["delay"] > 0: self.timer = reactor.callLater(transition["delay"], self._onEvent, transition["next_state"]) class XEP_0085_handler(XMPPHandler): implements(iwokkel.IDisco) def __init__(self, plugin_parent, profile): self.plugin_parent = plugin_parent self.host = plugin_parent.host self.profile = profile def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_CHAT_STATES)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0163.py0000755000175500017600000001500313243470025021442 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Personal Eventing Protocol (xep-0163) # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core import exceptions from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.xish import domish from wokkel import disco, pubsub from wokkel.formats import Mood NS_USER_MOOD = 'http://jabber.org/protocol/mood' PLUGIN_INFO = { C.PI_NAME: "Personal Eventing Protocol Plugin", C.PI_IMPORT_NAME: "XEP-0163", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0163", "XEP-0107"], C.PI_DEPENDENCIES: ["XEP-0060"], C.PI_MAIN: "XEP_0163", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Implementation of Personal Eventing Protocol""") } class XEP_0163(object): def __init__(self, host): log.info(_("PEP plugin initialization")) self.host = host self.pep_events = set() self.pep_out_cb = {} host.trigger.add("PubSub Disco Info", self.disoInfoTrigger) host.bridge.addMethod("PEPSend", ".plugin", in_sign='sa{ss}s', out_sign='', method=self.PEPSend, async=True) # args: type(MOOD, TUNE, etc), data, profile_key; self.addPEPEvent("MOOD", NS_USER_MOOD, self.userMoodCB, self.sendMood) def disoInfoTrigger(self, disco_info, profile): """Add info from managed PEP @param disco_info: list of disco feature as returned by PubSub, will be filled with PEP features @param profile: profile we are handling """ disco_info.extend(map(disco.DiscoFeature, self.pep_events)) return True def addPEPEvent(self, event_type, node, in_callback, out_callback=None, notify=True): """Add a Personal Eventing Protocol event manager @param event_type(unicode): type of the event (always uppercase), can be MOOD, TUNE, etc @param node(unicode): namespace of the node (e.g. http://jabber.org/protocol/mood for User Mood) @param in_callback(callable): method to call when this event occur the callable will be called with (itemsEvent, profile) as arguments @param out_callback(callable,None): method to call when we want to publish this event (must return a deferred) the callable will be called when sendPEPEvent is called @param notify(bool): add autosubscribe (+notify) if True """ if out_callback: self.pep_out_cb[event_type] = out_callback self.pep_events.add(node) if notify: self.pep_events.add(node + "+notify") def filterPEPEvent(client, itemsEvent): """Ignore messages which are not coming from PEP (i.e. main server) @param itemsEvent(pubsub.ItemsEvent): pubsub event """ if itemsEvent.sender.user or itemsEvent.sender.resource: log.debug("ignoring non PEP event from {} (profile={})".format(itemsEvent.sender.full(), client.profile)) return in_callback(itemsEvent, client.profile) self.host.plugins["XEP-0060"].addManagedNode(node, items_cb=filterPEPEvent) def sendPEPEvent(self, node, data, profile): """Publish the event data @param node(unicode): node namespace @param data: domish.Element to use as payload @param profile: profile which send the data """ client = self.host.getClient(profile) item = pubsub.Item(payload=data) return self.host.plugins["XEP-0060"].publish(client, None, node, [item]) def PEPSend(self, event_type, data, profile_key=C.PROF_KEY_NONE): """Send personal event after checking the data is alright @param event_type: type of event (eg: MOOD, TUNE), must be in self.pep_out_cb.keys() @param data: dict of {string:string} of event_type dependant data @param profile_key: profile who send the event """ profile = self.host.memory.getProfileName(profile_key) if not profile: log.error(_(u'Trying to send personal event with an unknown profile key [%s]') % profile_key) raise exceptions.ProfileUnknownError if not event_type in self.pep_out_cb.keys(): log.error(_('Trying to send personal event for an unknown type')) raise exceptions.DataError('Type unknown') return self.pep_out_cb[event_type](data, profile) def userMoodCB(self, itemsEvent, profile): if not itemsEvent.items: log.debug(_("No item found")) return try: mood_elt = [child for child in itemsEvent.items[0].elements() if child.name == "mood"][0] except IndexError: log.error(_("Can't find mood element in mood event")) return mood = Mood.fromXml(mood_elt) if not mood: log.debug(_("No mood found")) return self.host.bridge.psEvent(C.PS_PEP, itemsEvent.sender.full(), itemsEvent.nodeIdentifier, "MOOD", {"mood": mood.value or "", "text": mood.text or ""}, profile) def sendMood(self, data, profile): """Send XEP-0107's User Mood @param data: must include mood and text @param profile: profile which send the mood""" try: value = data['mood'].lower() text = data['text'] if 'text' in data else '' except KeyError: raise exceptions.DataError("Mood data must contain at least 'mood' key") mood = UserMood(value, text) return self.sendPEPEvent(NS_USER_MOOD, mood, profile) class UserMood(Mood, domish.Element): """Improved wokkel Mood which is also a domish.Element""" def __init__(self, value, text=None): Mood.__init__(self, value, text) domish.Element.__init__(self, (NS_USER_MOOD, 'mood')) self.addElement(value) if text: self.addElement('text', content=text) sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_maildir.py0000755000175500017600000005215613243470025022643 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT plugin for managing Maildir type mail boxes # 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 sat.core.i18n import D_, _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) import warnings warnings.filterwarnings('ignore', 'the MimeWriter', DeprecationWarning, 'twisted') # FIXME: to be removed, see http://twistedmatrix.com/trac/ticket/4038 from twisted.mail import maildir import email.message import email.utils import os from sat.core.exceptions import ProfileUnknownError from sat.memory.persistent import PersistentBinaryDict PLUGIN_INFO = { C.PI_NAME: "Maildir Plugin", C.PI_IMPORT_NAME: "Maildir", C.PI_TYPE: "Misc", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [], C.PI_MAIN: "MaildirBox", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Intercept "normal" type messages, and put them in a Maildir type box""") } MAILDIR_PATH = "Maildir" CATEGORY = D_("Mail Server") NAME = D_('Block "normal" messages propagation') # FIXME: (very) old and (very) experimental code, need a big cleaning/review or to be deprecated class MaildirError(Exception): pass class MaildirBox(object): params = """ """.format(category_name=CATEGORY, category_label=_(CATEGORY), name=NAME, label=_(NAME), ) def __init__(self, host): log.info(_("Plugin Maildir initialization")) self.host = host host.memory.updateParams(self.params) self.__observed = {} self.data = {} # list of profile spectific data. key = profile, value = PersistentBinaryDict where key=mailbox name, # and value is a dictionnary with the following value # - cur_idx: value of the current unique integer increment (UID) # - message_id (as returned by MaildirMailbox): a tuple of (UID, [flag1, flag2, ...]) self.__mailboxes = {} # key: profile, value: {boxname: MailboxUser instance} #the triggers host.trigger.add("MessageReceived", self.messageReceivedTrigger) def profileConnected(self, client): """Called on client connection, create profile data""" profile = client.profile self.data[profile] = PersistentBinaryDict("plugin_maildir", profile) self.__mailboxes[profile] = {} def dataLoaded(ignore): if not self.data[profile]: #the mailbox is new, we initiate the data self.data[profile]["INBOX"] = {"cur_idx": 0} self.data[profile].load().addCallback(dataLoaded) def profileDisconnected(self, client): """Called on profile disconnection, free profile's resources""" profile = client.profile del self.__mailboxes[profile] del self.data[profile] def messageReceivedTrigger(self, client, message, post_treat): """This trigger catch normal message and put the in the Maildir box. If the message is not of "normal" type, do nothing @param message: message xmlstrem @return: False if it's a normal message, True else""" profile = client.profile for e in message.elements(C.NS_CLIENT, 'body'): mess_type = message.getAttribute('type', 'normal') if mess_type != 'normal': return True self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile) return True def accessMessageBox(self, boxname, observer=None, profile_key=C.PROF_KEY_NONE): """Create and return a MailboxUser instance @param boxname: name of the box @param observer: method to call when a NewMessage arrive""" profile = self.host.memory.getProfileName(profile_key) if not profile: raise ProfileUnknownError(profile_key) if boxname not in self.__mailboxes[profile]: self.__mailboxes[profile][boxname] = MailboxUser(self, boxname, observer, profile=profile) else: if observer: self.addObserver(observer, profile, boxname) return self.__mailboxes[profile][boxname] def _getProfilePath(self, profile): """Return a unique path for profile's mailbox The path must be unique, usable as a dir name, and bijectional""" return profile.replace('/', '_').replace('..', '_') # FIXME: this is too naive to work well, must be improved def _removeBoxAccess(self, boxname, mailboxUser, profile): """Remove a reference to a box @param name: name of the box @param mailboxUser: MailboxUser instance""" if boxname not in self.__mailboxes: err_msg = _("Trying to remove an mailboxUser not referenced") log.error(_(u"INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) assert self.__mailboxes[profile][boxname] == mailboxUser del self.__mailboxes[profile][boxname] def _checkBoxReference(self, boxname, profile): """Check if there is a reference on a box, and return it @param boxname: name of the box to check @return: MailboxUser instance or None""" if profile in self.__mailboxes: if boxname in self.__mailboxes[profile]: return self.__mailboxes[profile][boxname] def __getBoxData(self, boxname, profile): """Return the date of a box""" try: return self.data[profile][boxname] # the boxname MUST exist in the data except KeyError: err_msg = _("Boxname doesn't exist in internal data") log.error(_(u"INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) def getUid(self, boxname, message_id, profile): """Return an unique integer, always ascending, for a message This is mainly needed for the IMAP protocol @param boxname: name of the box where the message is @param message_id: unique id of the message as given by MaildirMailbox @return: Integer UID""" box_data = self.__getBoxData(boxname, profile) if message_id in box_data: ret = box_data[message_id][0] else: box_data['cur_idx'] += 1 box_data[message_id] = [box_data['cur_idx'], []] ret = box_data[message_id] self.data[profile].force(boxname) return ret def getNextUid(self, boxname, profile): """Return next unique integer that will generated This is mainly needed for the IMAP protocol @param boxname: name of the box where the message is @return: Integer UID""" box_data = self.__getBoxData(boxname, profile) return box_data['cur_idx'] + 1 def getNextExistingUid(self, boxname, uid, profile): """Give the next uid of existing message @param boxname: name of the box where the message is @param uid: uid to start from @return: uid or None if the is no more message""" box_data = self.__getBoxData(boxname, profile) idx = uid + 1 while self.getIdFromUid(boxname, idx, profile) is None: # TODO: this is highly inefficient because getIdfromUid is inefficient, fix this idx += 1 if idx > box_data['cur_idx']: return None return idx def getMaxUid(self, boxname, profile): """Give the max existing uid @param boxname: name of the box where the message is @return: uid""" box_data = self.__getBoxData(boxname, profile) return box_data['cur_idx'] def getIdFromUid(self, boxname, message_uid, profile): """Return the message unique id from it's integer UID @param boxname: name of the box where the message is @param message_uid: unique integer identifier @return: unique id of the message as given by MaildirMailbox or None if not found""" box_data = self.__getBoxData(boxname, profile) for message_id in box_data.keys(): # TODO: this is highly inefficient on big mailbox, must be replaced in the future if message_id == 'cur_idx': continue if box_data[message_id][0] == message_uid: return message_id return None def getFlags(self, boxname, mess_id, profile): """Return the messages flags @param boxname: name of the box where the message is @param message_idx: message id as given by MaildirMailbox @return: list of strings""" box_data = self.__getBoxData(boxname, profile) if mess_id not in box_data: raise MaildirError("Trying to get flags from an unexisting message") return box_data[mess_id][1] def setFlags(self, boxname, mess_id, flags, profile): """Change the flags of the message @param boxname: name of the box where the message is @param message_idx: message id as given by MaildirMailbox @param flags: list of strings """ box_data = self.__getBoxData(boxname, profile) assert(type(flags) == list) flags = [flag.upper() for flag in flags] # we store every flag UPPERCASE if mess_id not in box_data: raise MaildirError("Trying to set flags for an unexisting message") box_data[mess_id][1] = flags self.data[profile].force(boxname) def getMessageIdsWithFlag(self, boxname, flag, profile): """Return ids of messages where a flag is set @param boxname: name of the box where the message is @param flag: flag to check @return: list of id (as given by MaildirMailbox)""" box_data = self.__getBoxData(boxname, profile) assert(isinstance(flag, basestring)) flag = flag.upper() result = [] for key in box_data: if key == 'cur_idx': continue if flag in box_data[key][1]: result.append(key) return result def purgeDeleted(self, boxname, profile): """Remove data for messages with flag "\\Deleted" @param boxname: name of the box where the message is """ box_data = self.__getBoxData(boxname, profile) for mess_id in self.getMessageIdsWithFlag(boxname, "\\Deleted", profile): del(box_data[mess_id]) self.data[profile].force(boxname) def cleanTable(self, boxname, existant_id, profile): """Remove mails which no longuer exist from the table @param boxname: name of the box to clean @param existant_id: list of id which actually exist""" box_data = self.__getBoxData(boxname, profile) to_remove = [] for key in box_data: if key not in existant_id and key != "cur_idx": to_remove.append(key) for key in to_remove: del box_data[key] def addObserver(self, callback, profile, boxname, signal="NEW_MESSAGE"): """Add an observer for maildir box changes @param callback: method to call when the the box is updated @param boxname: name of the box to observe @param signal: which signal is observed by the caller""" if (profile, boxname) not in self.__observed: self.__observed[(profile, boxname)] = {} if signal not in self.__observed[(profile, boxname)]: self.__observed[(profile, boxname)][signal] = set() self.__observed[(profile, boxname)][signal].add(callback) def removeObserver(self, callback, profile, boxname, signal="NEW_MESSAGE"): """Remove an observer of maildir box changes @param callback: method to remove from obervers @param boxname: name of the box which was observed @param signal: which signal was observed by the caller""" if (profile, boxname) not in self.__observed: err_msg = _(u"Trying to remove an observer for an inexistant mailbox") log.error(_(u"INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) if signal not in self.__observed[(profile, boxname)]: err_msg = _(u"Trying to remove an inexistant observer, no observer for this signal") log.error(_(u"INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) if not callback in self.__observed[(profile, boxname)][signal]: err_msg = _(u"Trying to remove an inexistant observer") log.error(_(u"INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) self.__observed[(profile, boxname)][signal].remove(callback) def emitSignal(self, profile, boxname, signal_name): """Emit the signal to observer""" log.debug(u'emitSignal %s %s %s' % (profile, boxname, signal_name)) try: for observer_cb in self.__observed[(profile, boxname)][signal_name]: observer_cb() except KeyError: pass class MailboxUser(object): """This class is used to access a mailbox""" def xmppMessage2mail(self, message): """Convert the XMPP's XML message to a basic rfc2822 message @param xml: domish.Element of the message @return: string email""" mail = email.message.Message() mail['MIME-Version'] = "1.0" mail['Content-Type'] = "text/plain; charset=UTF-8; format=flowed" mail['Content-Transfer-Encoding'] = "8bit" mail['From'] = message['from'].encode('utf-8') mail['To'] = message['to'].encode('utf-8') mail['Date'] = email.utils.formatdate().encode('utf-8') #TODO: save thread id for e in message.elements(): if e.name == "body": mail.set_payload(e.children[0].encode('utf-8')) elif e.name == "subject": mail['Subject'] = e.children[0].encode('utf-8') return mail.as_string() def __init__(self, _maildir, name, observer=None, profile=C.PROF_KEY_NONE): """@param _maildir: the main MaildirBox instance @param name: name of the mailbox @param profile: real profile (ie not a profile_key) THIS OBJECT MUST NOT BE USED DIRECTLY: use MaildirBox.accessMessageBox instead""" if _maildir._checkBoxReference(name, profile): log.error(u"INTERNAL ERROR: MailboxUser MUST NOT be instancied directly") raise MaildirError('double MailboxUser instanciation') if name != "INBOX": raise NotImplementedError self.name = name self.profile = profile self.maildir = _maildir profile_path = self.maildir._getProfilePath(profile) full_profile_path = os.path.join(self.maildir.host.memory.getConfig('', 'local_dir'), 'maildir', profile_path) if not os.path.exists(full_profile_path): os.makedirs(full_profile_path, 0700) mailbox_path = os.path.join(full_profile_path, MAILDIR_PATH) self.mailbox_path = mailbox_path self.mailbox = maildir.MaildirMailbox(mailbox_path) self.observer = observer self.__uid_table_update() if observer: log.debug(u"adding observer for %s (%s)" % (name, profile)) self.maildir.addObserver(observer, profile, name, "NEW_MESSAGE") def __uid_table_update(self): existant_id = [] for mess_idx in range(self.getMessageCount()): #we update the uid table existant_id.append(self.getId(mess_idx)) self.getUid(mess_idx) self.maildir.cleanTable(self.name, existant_id, profile=self.profile) def __del__(self): if self.observer: log.debug(u"removing observer for %s" % self.name) self._maildir.removeObserver(self.observer, self.name, "NEW_MESSAGE") self.maildir._removeBoxAccess(self.name, self, profile=self.profile) def addMessage(self, message): """Add a message to the box @param message: XMPP XML message""" self.mailbox.appendMessage(self.xmppMessage2mail(message)).addCallback(self.emitSignal, "NEW_MESSAGE") def emitSignal(self, ignore, signal): """Emit the signal to the observers""" if signal == "NEW_MESSAGE": self.getUid(self.getMessageCount() - 1) # XXX: we make an uid for the last message added self.maildir.emitSignal(self.profile, self.name, signal) def getId(self, mess_idx): """Return the Unique ID of the message @mess_idx: message index""" return self.mailbox.getUidl(mess_idx) def getUid(self, mess_idx): """Return a unique interger id for the message, always ascending""" mess_id = self.getId(mess_idx) return self.maildir.getUid(self.name, mess_id, profile=self.profile) def getNextUid(self): return self.maildir.getNextUid(self.name, profile=self.profile) def getNextExistingUid(self, uid): return self.maildir.getNextExistingUid(self.name, uid, profile=self.profile) def getMaxUid(self): return self.maildir.getMaxUid(self.name, profile=self.profile) def getMessageCount(self): """Return number of mails present in this box""" return len(self.mailbox.list) def getMessageIdx(self, mess_idx): """Return the full message @mess_idx: message index""" return self.mailbox.getMessage(mess_idx) def getIdxFromUid(self, mess_uid): """Return the message index from the uid @param mess_uid: message unique identifier @return: message index, as managed by MaildirMailbox""" for mess_idx in range(self.getMessageCount()): if self.getUid(mess_idx) == mess_uid: return mess_idx raise IndexError def getIdxFromId(self, mess_id): """Return the message index from the unique index @param mess_id: message unique index as given by MaildirMailbox @return: message sequence index""" for mess_idx in range(self.getMessageCount()): if self.mailbox.getUidl(mess_idx) == mess_id: return mess_idx raise IndexError def getMessage(self, mess_idx): """Return the full message @param mess_idx: message index""" return self.mailbox.getMessage(mess_idx) def getMessageUid(self, mess_uid): """Return the full message @param mess_idx: message unique identifier""" return self.mailbox.getMessage(self.getIdxFromUid(mess_uid)) def getFlags(self, mess_idx): """Return the flags of the message @param mess_idx: message index @return: list of strings""" id = self.getId(mess_idx) return self.maildir.getFlags(self.name, id, profile=self.profile) def getFlagsUid(self, mess_uid): """Return the flags of the message @param mess_uid: message unique identifier @return: list of strings""" id = self.maildir.getIdFromUid(self.name, mess_uid, profile=self.profile) return self.maildir.getFlags(self.name, id, profile=self.profile) def setFlags(self, mess_idx, flags): """Change the flags of the message @param mess_idx: message index @param flags: list of strings """ id = self.getId(mess_idx) self.maildir.setFlags(self.name, id, flags, profile=self.profile) def setFlagsUid(self, mess_uid, flags): """Change the flags of the message @param mess_uid: message unique identifier @param flags: list of strings """ id = self.maildir.getIdFromUid(self.name, mess_uid, profile=self.profile) return self.maildir.setFlags(self.name, id, flags, profile=self.profile) def getMessageIdsWithFlag(self, flag): """Return ids of messages where a flag is set @param flag: flag to check @return: list of id (as given by MaildirMailbox)""" return self.maildir.getMessageIdsWithFlag(self.name, flag, profile=self.profile) def removeDeleted(self): """Actually delete message flagged "\\Deleted" Also purge the internal data of these messages """ for mess_id in self.getMessageIdsWithFlag("\\Deleted"): print ("Deleting %s" % mess_id) self.mailbox.deleteMessage(self.getIdxFromId(mess_id)) self.mailbox = maildir.MaildirMailbox(self.mailbox_path) # We need to reparse the dir to have coherent indexing self.maildir.purgeDeleted(self.name, profile=self.profile) def emptyTrash(self): """Delete everything in the .Trash dir""" pass #TODO sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0050.py0000644000175500017600000006513513243470025021445 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Ad-Hoc Commands (XEP-0050) # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.protocols.jabber import jid from twisted.words.protocols import jabber from twisted.words.xish import domish from twisted.internet import defer from wokkel import disco, iwokkel, data_form, compat from sat.core import exceptions from sat.memory.memory import Sessions from uuid import uuid4 from sat.tools import xml_tools from zope.interface import implements try: from twisted.words.protocols.xmlstream import XMPPHandler except ImportError: from wokkel.subprotocols import XMPPHandler from collections import namedtuple try: from collections import OrderedDict # only available from python 2.7 except ImportError: from ordereddict import OrderedDict IQ_SET = '/iq[@type="set"]' NS_COMMANDS = "http://jabber.org/protocol/commands" ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list") ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' SHOWS = OrderedDict([('default', _('Online')), ('away', _('Away')), ('chat', _('Free for chat')), ('dnd', _('Do not disturb')), ('xa', _('Left')), ('disconnect', _('Disconnect'))]) PLUGIN_INFO = { C.PI_NAME: "Ad-Hoc Commands", C.PI_IMPORT_NAME: "XEP-0050", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0050"], C.PI_MAIN: "XEP_0050", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands""") } class AdHocError(Exception): def __init__(self, error_const): """ Error to be used from callback @param error_const: one of XEP_0050.ERROR """ assert error_const in XEP_0050.ERROR self.callback_error = error_const class AdHocCommand(XMPPHandler): implements(iwokkel.IDisco) def __init__(self, parent, callback, label, node, features, timeout, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client): self.parent = parent self.callback = callback self.label = label self.node = node self.features = [disco.DiscoFeature(feature) for feature in features] self.allowed_jids, self.allowed_groups, self.allowed_magics, self.forbidden_jids, self.forbidden_groups = allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups self.client = client self.sessions = Sessions(timeout=timeout) def getName(self, xml_lang=None): return self.label def isAuthorised(self, requestor): if '@ALL@' in self.allowed_magics: return True forbidden = set(self.forbidden_jids) for group in self.forbidden_groups: forbidden.update(self.client.roster.getJidsFromGroup(group)) if requestor.userhostJID() in forbidden: return False allowed = set(self.allowed_jids) for group in self.allowed_groups: try: allowed.update(self.client.roster.getJidsFromGroup(group)) except exceptions.UnknownGroupError: log.warning(_(u"The groups [%(group)s] is unknown for profile [%(profile)s])" % {'group':group, 'profile':self.client.profile})) if requestor.userhostJID() in allowed: return True return False def getDiscoInfo(self, requestor, target, nodeIdentifier=''): if nodeIdentifier != NS_COMMANDS: # FIXME: we should manage other disco nodes here return [] # identities = [ID_CMD_LIST if self.node == NS_COMMANDS else ID_CMD_NODE] # FIXME return [disco.DiscoFeature(NS_COMMANDS)] + self.features def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] def _sendAnswer(self, callback_data, session_id, request): """ Send result of the command @param callback_data: tuple (payload, status, actions, note) with: - payload (domish.Element) usualy containing data form - status: current status, see XEP_0050.STATUS - actions: list of allowed actions (see XEP_0050.ACTION). First action is the default one. Default to EXECUTE - note: optional additional note: either None or a tuple with (note type, human readable string), note type being in XEP_0050.NOTE @param session_id: current session id @param request: original request (domish.Element) @return: deferred """ payload, status, actions, note = callback_data assert(isinstance(payload, domish.Element) or payload is None) assert(status in XEP_0050.STATUS) if not actions: actions = [XEP_0050.ACTION.EXECUTE] result = domish.Element((None, 'iq')) result['type'] = 'result' result['id'] = request['id'] result['to'] = request['from'] command_elt = result.addElement('command', NS_COMMANDS) command_elt['sessionid'] = session_id command_elt['node'] = self.node command_elt['status'] = status if status != XEP_0050.STATUS.CANCELED: if status != XEP_0050.STATUS.COMPLETED: actions_elt = command_elt.addElement('actions') actions_elt['execute'] = actions[0] for action in actions: actions_elt.addElement(action) if note is not None: note_type, note_mess = note note_elt = command_elt.addElement('note', content=note_mess) note_elt['type'] = note_type if payload is not None: command_elt.addChild(payload) self.client.send(result) if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): del self.sessions[session_id] def _sendError(self, error_constant, session_id, request): """ Send error stanza @param error_constant: one of XEP_OO50.ERROR @param request: original request (domish.Element) """ xmpp_condition, cmd_condition = error_constant iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) if cmd_condition: error_elt = iq_elt.elements(None, "error").next() error_elt.addElement(cmd_condition, NS_COMMANDS) self.client.send(iq_elt) del self.sessions[session_id] def onRequest(self, command_elt, requestor, action, session_id): if not self.isAuthorised(requestor): return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) if session_id: try: session_data = self.sessions[session_id] except KeyError: return self._sendError(XEP_0050.ERROR.SESSION_EXPIRED, session_id, command_elt.parent) if session_data['requestor'] != requestor: return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) else: session_id, session_data = self.sessions.newSession() session_data['requestor'] = requestor if action == XEP_0050.ACTION.CANCEL: d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None)) else: d = defer.maybeDeferred(self.callback, command_elt, session_data, action, self.node, self.client.profile) d.addCallback(self._sendAnswer, session_id, command_elt.parent) d.addErrback(lambda failure, request: self._sendError(failure.value.callback_error, session_id, request), command_elt.parent) class XEP_0050(object): STATUS = namedtuple('Status', ('EXECUTING', 'COMPLETED', 'CANCELED'))('executing', 'completed', 'canceled') ACTION = namedtuple('Action', ('EXECUTE', 'CANCEL', 'NEXT', 'PREV'))('execute', 'cancel', 'next', 'prev') NOTE = namedtuple('Note', ('INFO','WARN','ERROR'))('info','warn','error') ERROR = namedtuple('Error', ('MALFORMED_ACTION', 'BAD_ACTION', 'BAD_LOCALE', 'BAD_PAYLOAD', 'BAD_SESSIONID', 'SESSION_EXPIRED', 'FORBIDDEN', 'ITEM_NOT_FOUND', 'FEATURE_NOT_IMPLEMENTED', 'INTERNAL'))(('bad-request', 'malformed-action'), ('bad-request', 'bad-action'), ('bad-request', 'bad-locale'), ('bad-request','bad-payload'), ('bad-request','bad-sessionid'), ('not-allowed','session-expired'), ('forbidden', None), ('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.4 Table 5 def __init__(self, host): log.info(_("plugin XEP-0050 initialization")) self.host = host self.requesting = Sessions() self.answering = {} host.bridge.addMethod("adHocRun", ".plugin", in_sign='sss', out_sign='s', method=self._run, async=True) host.bridge.addMethod("adHocList", ".plugin", in_sign='ss', out_sign='s', method=self._list, async=True) self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True) host.importMenu((D_("Service"), D_("Commands")), self._commandsMenu, security_limit=2, help_string=D_("Execute ad-hoc commands")) def getHandler(self, client): return XEP_0050_handler(self) def profileConnected(self, client): self.addAdHocCommand(self._statusCallback, _("Status"), profile_key=client.profile) def profileDisconnected(self, client): try: del self.answering[client.profile] except KeyError: pass def _items2XMLUI(self, items, no_instructions): """ Convert discovery items to XMLUI dialog """ # TODO: manage items on different jids form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) if not no_instructions: form_ui.addText(_("Please select a command"), 'instructions') options = [(item.nodeIdentifier, item.name) for item in items] form_ui.addList("node", options) return form_ui def _getDataLvl(self, type_): """Return the constant corresponding to type attribute value @param type_: note type (see XEP-0050 §4.3) @return: a C.XMLUI_DATA_LVL_* constant """ if type_ == 'error': return C.XMLUI_DATA_LVL_ERROR elif type_ == 'warn': return C.XMLUI_DATA_LVL_WARNING else: if type_ != 'info': log.warning(_(u"Invalid note type [%s], using info") % type_) return C.XMLUI_DATA_LVL_INFO def _mergeNotes(self, notes): """Merge notes with level prefix (e.g. "ERROR: the message") @param notes (list): list of tuple (level, message) @return: list of messages """ lvl_map = {C.XMLUI_DATA_LVL_INFO: '', C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR") } return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): """ Convert command answer to an ui for frontend @param iq_elt: command result @param session_id: id of the session used with the frontend @param profile_key: %(doc_profile_key)s """ command_elt = iq_elt.elements(NS_COMMANDS, "command").next() status = command_elt.getAttribute('status', XEP_0050.STATUS.EXECUTING) if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: # the command session is finished, we purge our session del self.requesting[session_id] if status == XEP_0050.STATUS.COMPLETED: session_id = None else: return None remote_session_id = command_elt.getAttribute('sessionid') if remote_session_id: session_data['remote_id'] = remote_session_id notes = [] for note_elt in command_elt.elements(NS_COMMANDS, 'note'): notes.append((self._getDataLvl(note_elt.getAttribute('type', 'info')), unicode(note_elt))) try: data_elt = command_elt.elements(data_form.NS_X_DATA, 'x').next() except StopIteration: if status != XEP_0050.STATUS.COMPLETED: log.warning(_("No known payload found in ad-hoc command result, aborting")) del self.requesting[session_id] return xml_tools.XMLUI(C.XMLUI_DIALOG, dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, C.XMLUI_DATA_MESS: _("No payload found"), C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_ERROR, } ) if not notes: # the status is completed, and we have no note to show return None # if we have only one note, we show a dialog with the level of the note # if we have more, we show a dialog with "info" level, and all notes merged dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO return xml_tools.XMLUI( C.XMLUI_DIALOG, dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, C.XMLUI_DATA_MESS: u'\n'.join(self._mergeNotes(notes)), C.XMLUI_DATA_LVL: dlg_level, }, session_id = session_id ) if session_id is None: return xml_tools.dataFormEltResult2XMLUI(data_elt) form = data_form.Form.fromElement(data_elt) # we add any present note to the instructions form.instructions.extend(self._mergeNotes(notes)) return xml_tools.dataForm2XMLUI(form, self.__requesting_id, session_id=session_id) def _requestingEntity(self, data, profile): def serialise(ret_data): if 'xmlui' in ret_data: ret_data['xmlui'] = ret_data['xmlui'].toXml() return ret_data d = self.requestingEntity(data, profile) d.addCallback(serialise) return d def requestingEntity(self, data, profile): """ request and entity and create XMLUI accordingly @param data: data returned by previous XMLUI (first one must come from self._commandsMenu) @param profile: %(doc_profile)s @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error) """ if C.bool(data.get('cancelled', C.BOOL_FALSE)): return defer.succeed({}) client = self.host.getClient(profile) # TODO: cancel, prev and next are not managed # TODO: managed answerer errors # TODO: manage nodes with a non data form payload if "session_id" not in data: # we just had the jid, we now request it for the available commands session_id, session_data = self.requesting.newSession(profile=client.profile) entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX+'jid']) session_data['jid'] = entity d = self.list(client, entity) def sendItems(xmlui): xmlui.session_id = session_id # we need to keep track of the session return {'xmlui': xmlui} d.addCallback(sendItems) else: # we have started a several forms sessions try: session_data = self.requesting.profileGet(data["session_id"], client.profile) except KeyError: log.warning ("session id doesn't exist, session has probably expired") # TODO: send error dialog return defer.succeed({}) session_id = data["session_id"] entity = session_data['jid'] try: session_data['node'] # node has already been received except KeyError: # it's the first time we know the node, we save it in session data session_data['node'] = data[xml_tools.SAT_FORM_PREFIX+'node'] # we request execute node's command iq_elt = compat.IQ(client.xmlstream, 'set') iq_elt['to'] = entity.full() command_elt = iq_elt.addElement("command", NS_COMMANDS) command_elt['node'] = session_data['node'] command_elt['action'] = XEP_0050.ACTION.EXECUTE try: # remote_id is the XEP_0050 sessionid used by answering command # while session_id is our own session id used with the frontend command_elt['sessionid'] = session_data['remote_id'] except KeyError: pass command_elt.addChild(xml_tools.XMLUIResultToElt(data)) # We add the XMLUI result to the command payload d = iq_elt.send() d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) d.addCallback(lambda xmlui: {'xmlui': xmlui} if xmlui is not None else {}) return d def _commandsMenu(self, menu_data, profile): """ First XMLUI activated by menu: ask for target jid @param profile: %(doc_profile)s """ form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) form_ui.addText(_("Please enter target jid"), 'instructions') form_ui.changeContainer("pairs") form_ui.addLabel("jid") form_ui.addString("jid", value=self.host.getClient(profile).jid.host) return {'xmlui': form_ui.toXml()} def _statusCallback(self, command_elt, session_data, action, node, profile): """ Ad-hoc command used to change the "show" part of status """ actions = session_data.setdefault('actions',[]) actions.append(action) if len(actions) == 1: # it's our first request, we ask the desired new status status = XEP_0050.STATUS.EXECUTING form = data_form.Form('form', title=_('status selection')) show_options = [data_form.Option(name, label) for name, label in SHOWS.items()] field = data_form.Field('list-single', 'show', options=show_options, required=True) form.addField(field) payload = form.toElement() note = None elif len(actions) == 2: # we should have the answer here try: x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next() answer_form = data_form.Form.fromElement(x_elt) show = answer_form['show'] except (KeyError, StopIteration): raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) if show not in SHOWS: raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) if show == "disconnect": self.host.disconnect(profile) else: self.host.setPresence(show=show, profile_key=profile) # job done, we can end the session form = data_form.Form('form', title=_(u'Updated')) form.addField(data_form.Field('fixed', u'Status updated')) status = XEP_0050.STATUS.COMPLETED payload = None note = (self.NOTE.INFO, _(u"Status updated")) else: raise AdHocError(XEP_0050.ERROR.INTERNAL) return (payload, status, None, note) def _run(self, service_jid_s='', node='', profile_key=C.PROF_KEY_NONE): client = self.host.getClient(profile_key) service_jid = jid.JID(service_jid_s) if service_jid_s else None d = self.run(client, service_jid, node or None) d.addCallback(lambda xmlui: xmlui.toXml()) return d @defer.inlineCallbacks def run(self, client, service_jid=None, node=None): """run an ad-hoc command @param service_jid(jid.JID, None): jid of the ad-hoc service None to use profile's server @param node(unicode, None): node of the ad-hoc commnad None to get initial list @return(unicode): command page XMLUI """ if service_jid is None: service_jid = jid.JID(client.jid.host) session_id, session_data = self.requesting.newSession(profile=client.profile) session_data['jid'] = service_jid if node is None: xmlui = yield self.list(client, service_jid) else: session_data['node'] = node cb_data = yield self.requestingEntity({'session_id': session_id}, client.profile) xmlui = cb_data['xmlui'] xmlui.session_id = session_id defer.returnValue(xmlui) def _list(self, to_jid_s, profile_key): client = self.host.getClient(profile_key) to_jid = jid.JID(to_jid_s) if to_jid_s else None d = self.list(client, to_jid, no_instructions=True) d.addCallback(lambda xmlui: xmlui.toXml()) return d def list(self, client, to_jid, no_instructions=False): """Request available commands @param to_jid(jid.JID, None): the entity answering the commands None to use profile's server @param no_instructions(bool): if True, don't add instructions widget """ d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) d.addCallback(self._items2XMLUI, no_instructions) return d def addAdHocCommand(self, callback, label, node=None, features=None, timeout=600, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None, profile_key=C.PROF_KEY_NONE): """Add an ad-hoc command for the current profile @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred @param label: label associated with this command on the main menu @param node: disco item node associated with this command. None to use autogenerated node @param features: features associated with the payload (list of strings), usualy data form @param timeout: delay between two requests before canceling the session (in seconds) @param allowed_jids: list of allowed entities @param allowed_groups: list of allowed roster groups @param allowed_magics: list of allowed magic keys, can be: @ALL@: allow everybody @PROFILE_BAREJID@: allow only the jid of the profile @param forbidden_jids: black list of entities which can't access this command @param forbidden_groups: black list of groups which can't access this command @param profile_key: profile key associated with this command, @ALL@ means can be accessed with every profiles @return: node of the added command, useful to remove the command later """ # FIXME: "@ALL@" for profile_key seems useless and dangerous if node is None: node = "%s_%s" % ('COMMANDS', uuid4()) if features is None: features = [data_form.NS_X_DATA] if allowed_jids is None: allowed_jids = [] if allowed_groups is None: allowed_groups = [] if allowed_magics is None: allowed_magics = ['@PROFILE_BAREJID@'] if forbidden_jids is None: forbidden_jids = [] if forbidden_groups is None: forbidden_groups = [] for client in self.host.getClients(profile_key): #TODO: manage newly created/removed profiles _allowed_jids = (allowed_jids + [client.jid.userhostJID()]) if '@PROFILE_BAREJID@' in allowed_magics else allowed_jids ad_hoc_command = AdHocCommand(self, callback, label, node, features, timeout, _allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client) ad_hoc_command.setHandlerParent(client) profile_commands = self.answering.setdefault(client.profile, {}) profile_commands[node] = ad_hoc_command def onCmdRequest(self, request, profile): request.handled = True requestor = jid.JID(request['from']) command_elt = request.elements(NS_COMMANDS, 'command').next() action = command_elt.getAttribute('action', self.ACTION.EXECUTE) node = command_elt.getAttribute('node') if not node: raise exceptions.DataError sessionid = command_elt.getAttribute('sessionid') try: command = self.answering[profile][node] except KeyError: raise exceptions.DataError command.onRequest(command_elt, requestor, action, sessionid) class XEP_0050_handler(XMPPHandler): implements(iwokkel.IDisco) def __init__(self, plugin_parent): self.plugin_parent = plugin_parent def connectionInitialized(self): self.xmlstream.addObserver(CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): identities = [] if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get(self.parent.profile): # we only add the identity if we have registred commands identities.append(ID_CMD_LIST) return [disco.DiscoFeature(NS_COMMANDS)] + identities def getDiscoItems(self, requestor, target, nodeIdentifier=''): ret = [] if nodeIdentifier == NS_COMMANDS: for command in self.plugin_parent.answering.get(self.parent.profile,{}).values(): if command.isAuthorised(requestor): ret.append(disco.DiscoItem(self.parent.jid, command.node, command.getName())) #TODO: manage name language return ret sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_extra_pep.py0000644000175500017600000000447213243470024023203 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for displaying messages from extra PEP services # Copyright (C) 2015 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.memory import params from twisted.words.protocols.jabber import jid PLUGIN_INFO = { C.PI_NAME: "Extra PEP", C.PI_IMPORT_NAME: "EXTRA-PEP", C.PI_TYPE: "MISC", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [], C.PI_RECOMMENDATIONS: [], C.PI_MAIN: "ExtraPEP", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _(u"""Display messages from extra PEP services""") } PARAM_KEY = u"Misc" PARAM_NAME = u"blogs" PARAM_LABEL = u"Blog authors following list" PARAM_DEFAULT = (jid.JID("salut-a-toi@libervia.org"),) class ExtraPEP(object): params = """ %(jids)s """ % { 'category_name': PARAM_KEY, 'category_label': D_(PARAM_KEY), 'param_name': PARAM_NAME, 'param_label': D_(PARAM_LABEL), 'jids': u"\n".join({elt.toXml() for elt in params.createJidElts(PARAM_DEFAULT)}) } def __init__(self, host): log.info(_(u"Plugin Extra PEP initialization")) self.host = host host.memory.updateParams(self.params) def getFollowedEntities(self, profile_key): return self.host.memory.getParamA(PARAM_NAME, PARAM_KEY, profile_key=profile_key) sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0049.py0000755000175500017600000000564213243470025021455 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing xep-0049 # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from wokkel import compat from twisted.words.xish import domish PLUGIN_INFO = { C.PI_NAME: "XEP-0049 Plugin", C.PI_IMPORT_NAME: "XEP-0049", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0049"], C.PI_DEPENDENCIES: [], C.PI_MAIN: "XEP_0049", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Implementation of private XML storage""") } class XEP_0049(object): NS_PRIVATE = 'jabber:iq:private' def __init__(self, host): log.info(_("Plugin XEP-0049 initialization")) self.host = host def privateXMLStore(self, element, profile_key): """Store private data @param element: domish.Element to store (must have a namespace) @param profile_key: %(doc_profile_key)s """ assert isinstance(element, domish.Element) client = self.host.getClient(profile_key) # XXX: feature announcement in disco#info is not mandatory in XEP-0049, so we have to try to use private XML, and react according to the answer iq_elt = compat.IQ(client.xmlstream) query_elt = iq_elt.addElement('query', XEP_0049.NS_PRIVATE) query_elt.addChild(element) return iq_elt.send() def privateXMLGet(self, node_name, namespace, profile_key): """Store private data @param node_name: name of the node to get @param namespace: namespace of the node to get @param profile_key: %(doc_profile_key)s @return (domish.Element): a deferred which fire the stored data """ client = self.host.getClient(profile_key) # XXX: see privateXMLStore note about feature checking iq_elt = compat.IQ(client.xmlstream, 'get') query_elt = iq_elt.addElement('query', XEP_0049.NS_PRIVATE) query_elt.addElement(node_name, namespace) def getCb(answer_iq_elt): answer_query_elt = answer_iq_elt.elements(XEP_0049.NS_PRIVATE, 'query').next() return answer_query_elt.firstChildElement() d = iq_elt.send() d.addCallback(getCb) return d sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_upload.py0000644000175500017600000001432713243470025022501 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for file tansfer # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from sat.tools import xml_tools from twisted.internet import defer from twisted.words.protocols.jabber import jid import os import os.path PLUGIN_INFO = { C.PI_NAME: "File Upload", C.PI_IMPORT_NAME: "UPLOAD", C.PI_TYPE: C.PLUG_TYPE_MISC, C.PI_MAIN: "UploadPlugin", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""File upload management""") } UPLOADING = D_(u'Please select a file to upload') UPLOADING_TITLE = D_(u'File upload') BOOL_OPTIONS = ('ignore_tls_errors',) class UploadPlugin(object): # TODO: plugin unload def __init__(self, host): log.info(_("plugin Upload initialization")) self.host = host host.bridge.addMethod("fileUpload", ".plugin", in_sign='sssa{ss}s', out_sign='a{ss}', method=self._fileUpload, async=True) self._upload_callbacks = [] def _fileUpload(self, filepath, filename, upload_jid_s='', options=None, profile=C.PROF_KEY_NONE): client = self.host.getClient(profile) upload_jid = jid.JID(upload_jid_s) if upload_jid_s else None if options is None: options = {} # we convert values that are well-known booleans for bool_option in BOOL_OPTIONS: try: options[bool_option] = C.bool(options[bool_option]) except KeyError: pass return self.fileUpload(client, filepath, filename or None, upload_jid, options or None) def fileUpload(self, client, filepath, filename, upload_jid, options): """Send a file using best available method parameters are the same as for [upload] @return (dict): action dictionary, with progress id in case of success, else xmlui message """ def uploadCb(data): progress_id, dummy = data return {'progress': progress_id} def uploadEb(fail): msg = unicode(fail) log.warning(msg) return {'xmlui': xml_tools.note(u"Can't upload file", msg, C.XMLUI_DATA_LVL_WARNING).toXml()} d = self.upload(client, filepath, filename, upload_jid, options) d.addCallback(uploadCb) d.addErrback(uploadEb) return d @defer.inlineCallbacks def upload(self, client, filepath, filename=None, upload_jid=None, options=None): """Send a file using best available method @param filepath(str): absolute path to the file @param filename(None, unicode): name to use for the upload None to use basename of the path @param upload_jid(jid.JID, None): upload capable entity jid, or None to use autodetected, if possible @param options(dict): option to use for the upload, may be: - ignore_tls_errors(bool): True to ignore SSL/TLS certificate verification used only if HTTPS transport is needed @param profile: %(doc_profile)s @return (tuple[unicode,D(unicode)]): progress_id and a Deferred which fire download URL when upload is finished """ if options is None: options = {} if not os.path.isfile(filepath): raise exceptions.DataError(u"The given path doesn't link to a file") for method_name, available_cb, upload_cb, priority in self._upload_callbacks: try: upload_jid = yield available_cb(upload_jid, client.profile) except exceptions.NotFound: continue # no entity managing this extension found log.info(u"{name} method will be used to upload the file".format(name=method_name)) progress_id_d, download_d = yield upload_cb(filepath, filename, upload_jid, options, client.profile) progress_id = yield progress_id_d defer.returnValue((progress_id, download_d)) raise exceptions.NotFound(u"Can't find any method to upload a file") def register(self, method_name, available_cb, upload_cb, priority=0): """Register a fileUploading method @param method_name(unicode): short name for the method, must be unique @param available_cb(callable): method to call to check if this method is usable the callback must take two arguments: upload_jid (can be None) and profile the callback must return the first entity found (being upload_jid or one of its components) exceptions.NotFound must be raised if no entity has been found @param upload_cb(callable): method to upload a file must have the same signature as [fileUpload] must return a tuple with progress_id and a Deferred which fire download URL when upload is finished @param priority(int): pririoty of this method, the higher available will be used """ assert method_name for data in self._upload_callbacks: if method_name == data[0]: raise exceptions.ConflictError(u'A method with this name is already registered') self._upload_callbacks.append((method_name, available_cb, upload_cb, priority)) self._upload_callbacks.sort(key=lambda data: data[3], reverse=True) def unregister(self, method_name): for idx, data in enumerate(self._upload_callbacks): if data[0] == method_name: del [idx] return raise exceptions.NotFound(u"The name to unregister doesn't exist") sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0100.py0000755000175500017600000002222513243470025021435 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing gateways (xep-0100) # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core import exceptions from sat.tools import xml_tools from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.protocols.jabber import jid from twisted.internet import reactor, defer PLUGIN_INFO = { C.PI_NAME: "Gateways Plugin", C.PI_IMPORT_NAME: "XEP-0100", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0100"], C.PI_DEPENDENCIES: ["XEP-0077"], C.PI_MAIN: "XEP_0100", C.PI_DESCRIPTION: _("""Implementation of Gateways protocol""") } WARNING_MSG = D_(u"""Be careful ! Gateways allow you to use an external IM (legacy IM), so you can see your contact as XMPP contacts. But when you do this, all your messages go throught the external legacy IM server, it is a huge privacy issue (i.e.: all your messages throught the gateway can be monitored, recorded, analysed by the external server, most of time a private company).""") GATEWAY_TIMEOUT = 10 # time to wait before cancelling a gateway disco info, in seconds TYPE_DESCRIPTIONS = {'irc': D_("Internet Relay Chat"), 'xmpp': D_("XMPP"), 'qq': D_("Tencent QQ"), 'simple': D_("SIP/SIMPLE"), 'icq': D_("ICQ"), 'yahoo': D_("Yahoo! Messenger"), 'gadu-gadu': D_("Gadu-Gadu"), 'aim': D_("AOL Instant Messenger"), 'msn': D_("Windows Live Messenger"), } class XEP_0100(object): def __init__(self, host): log.info(_("Gateways plugin initialization")) self.host = host self.__gateways = {} # dict used to construct the answer to findGateways. Key = target jid host.bridge.addMethod("findGateways", ".plugin", in_sign='ss', out_sign='s', method=self._findGateways) host.bridge.addMethod("gatewayRegister", ".plugin", in_sign='ss', out_sign='s', method=self._gatewayRegister) self.__menu_id = host.registerCallback(self._gatewaysMenu, with_data=True) self.__selected_id = host.registerCallback(self._gatewaySelectedCb, with_data=True) host.importMenu((D_("Service"), D_("Gateways")), self._gatewaysMenu, security_limit=1, help_string=D_("Find gateways")) def _gatewaysMenu(self, data, profile): """ XMLUI activated by menu: return Gateways UI @param profile: %(doc_profile)s """ client = self.host.getClient(profile) try: jid_ = jid.JID(data.get(xml_tools.formEscape('external_jid'), client.jid.host)) except RuntimeError: raise exceptions.DataError(_("Invalid JID")) d = self.findGateways(jid_, profile) d.addCallback(self._gatewaysResult2XMLUI, jid_) d.addCallback(lambda xmlui: {'xmlui': xmlui.toXml()}) return d def _gatewaysResult2XMLUI(self, result, entity): xmlui = xml_tools.XMLUI(title=_('Gateways manager (%s)') % entity.full()) xmlui.addText(_(WARNING_MSG)) xmlui.addDivider('dash') adv_list = xmlui.changeContainer('advanced_list', columns=3, selectable='single', callback_id=self.__selected_id) for success, gateway_data in result: if not success: fail_cond, disco_item = gateway_data xmlui.addJid(disco_item.entity) xmlui.addText(_('Failed (%s)') % fail_cond) xmlui.addEmpty() else: jid_, data = gateway_data for datum in data: identity, name = datum adv_list.setRowIndex(jid_.full()) xmlui.addJid(jid_) xmlui.addText(name) xmlui.addText(self._getIdentityDesc(identity)) adv_list.end() xmlui.addDivider('blank') xmlui.changeContainer('advanced_list', columns=3) xmlui.addLabel(_('Use external XMPP server')) xmlui.addString('external_jid') xmlui.addButton(self.__menu_id, _(u'Go !'), fields_back=('external_jid',)) return xmlui def _gatewaySelectedCb(self, data, profile): try: target_jid = jid.JID(data['index']) except (KeyError, RuntimeError): log.warning(_("No gateway index selected")) return {} d = self.gatewayRegister(target_jid, profile) d.addCallback(lambda xmlui: {'xmlui': xmlui.toXml()}) return d def _getIdentityDesc(self, identity): """ Return a human readable description of identity @param identity: tuple as returned by Disco identities (category, type) """ category, type_ = identity if category != 'gateway': log.error(_(u'INTERNAL ERROR: identity category should always be "gateway" in _getTypeString, got "%s"') % category) try: return _(TYPE_DESCRIPTIONS[type_]) except KeyError: return _("Unknown IM") def _registrationSuccessful(self, jid_, profile): """Called when in_band registration is ok, we must now follow the rest of procedure""" log.debug(_("Registration successful, doing the rest")) self.host.addContact(jid_, profile_key=profile) self.host.setPresence(jid_, profile_key=profile) def _gatewayRegister(self, target_jid_s, profile_key=C.PROF_KEY_NONE): d = self.gatewayRegister(jid.JID(target_jid_s), profile_key) d.addCallback(lambda xmlui: xmlui.toXml()) return d def gatewayRegister(self, target_jid, profile_key=C.PROF_KEY_NONE): """Register gateway using in-band registration, then log-in to gateway""" profile = self.host.memory.getProfileName(profile_key) assert(profile) d = self.host.plugins["XEP-0077"].inBandRegister(target_jid, self._registrationSuccessful, profile) return d def _infosReceived(self, dl_result, items, target, client): """Find disco infos about entity, to check if it is a gateway""" ret = [] for idx, (success, result) in enumerate(dl_result): if not success: if isinstance(result.value, defer.CancelledError): msg = _("Timeout") else: try: msg = result.value.condition except AttributeError: msg = str(result) ret.append((success, (msg, items[idx]))) else: entity = items[idx].entity gateways = [(identity, result.identities[identity]) for identity in result.identities if identity[0] == 'gateway'] if gateways: log.info(_(u"Found gateway [%(jid)s]: %(identity_name)s") % {'jid': entity.full(), 'identity_name': ' - '.join([gateway[1] for gateway in gateways])}) ret.append((success, (entity, gateways))) else: log.info(_(u"Skipping [%(jid)s] which is not a gateway") % {'jid': entity.full()}) return ret def _itemsReceived(self, disco, target, client): """Look for items with disco protocol, and ask infos for each one""" if len(disco._items) == 0: log.debug(_("No gateway found")) return [] _defers = [] for item in disco._items: log.debug(_(u"item found: %s") % item.entity) _defers.append(client.disco.requestInfo(item.entity)) dl = defer.DeferredList(_defers) dl.addCallback(self._infosReceived, items=disco._items, target=target, client=client) reactor.callLater(GATEWAY_TIMEOUT, dl.cancel) return dl def _findGateways(self, target_jid_s, profile_key): target_jid = jid.JID(target_jid_s) profile = self.host.memory.getProfileName(profile_key) if not profile: raise exceptions.ProfileUnknownError d = self.findGateways(target_jid, profile) d.addCallback(self._gatewaysResult2XMLUI, target_jid) d.addCallback(lambda xmlui: xmlui.toXml()) return d def findGateways(self, target, profile): """Find gateways in the target JID, using discovery protocol """ client = self.host.getClient(profile) log.debug(_(u"find gateways (target = %(target)s, profile = %(profile)s)") % {'target': target.full(), 'profile': profile}) d = client.disco.requestItems(target) d.addCallback(self._itemsReceived, target=target, client=client) return d sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0234.py0000644000175500017600000003646113243470025021451 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for Jingle File Transfer (XEP-0234) # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from wokkel import disco, iwokkel from zope.interface import implements from sat.tools import utils from sat.tools import stream import os.path from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from twisted.python import failure from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.internet import defer from twisted.internet import reactor from twisted.internet import error as internet_error NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4' PLUGIN_INFO = { C.PI_NAME: "Jingle File Transfer", C.PI_IMPORT_NAME: "XEP-0234", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0234"], C.PI_DEPENDENCIES: ["XEP-0166", "XEP-0300", "FILE"], C.PI_MAIN: "XEP_0234", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Jingle File Transfer""") } class XEP_0234(object): # TODO: assure everything is closed when file is sent or session terminate is received # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) def __init__(self, host): log.info(_("plugin Jingle File Transfer initialization")) self.host = host self._j = host.plugins["XEP-0166"] # shortcut to access jingle self._j.registerApplication(NS_JINGLE_FT, self) self._f = host.plugins["FILE"] self._f.register(NS_JINGLE_FT, self.fileJingleSend, priority = 10000, method_name=u"Jingle") self._hash = self.host.plugins["XEP-0300"] host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend) def getHandler(self, client): return XEP_0234_handler() def _getProgressId(self, session, content_name): """Return a unique progress ID @param session(dict): jingle session @param content_name(unicode): name of the content @return (unicode): unique progress id """ return u'{}_{}'.format(session['id'], content_name) def _fileJingleSend(self, peer_jid, filepath, name="", file_desc="", profile=C.PROF_KEY_NONE): client = self.host.getClient(profile) return self.fileJingleSend(client, jid.JID(peer_jid), filepath, name or None, file_desc or None) def fileJingleSend(self, client, peer_jid, filepath, name, file_desc=None): """Send a file using jingle file transfer @param peer_jid(jid.JID): destinee jid @param filepath(str): absolute path of the file @param name(unicode, None): name of the file @param file_desc(unicode, None): description of the file @return (D(unicode)): progress id """ progress_id_d = defer.Deferred() self._j.initiate(client, peer_jid, [{'app_ns': NS_JINGLE_FT, 'senders': self._j.ROLE_INITIATOR, 'app_kwargs': {'filepath': filepath, 'name': name, 'file_desc': file_desc, 'progress_id_d': progress_id_d}, }]) return progress_id_d # jingle callbacks def jingleSessionInit(self, client, session, content_name, filepath, name, file_desc, progress_id_d): progress_id_d.callback(self._getProgressId(session, content_name)) content_data = session['contents'][content_name] application_data = content_data['application_data'] assert 'file_path' not in application_data application_data['file_path'] = filepath file_data = application_data['file_data'] = {} file_data['date'] = utils.xmpp_date() file_data['desc'] = file_desc or '' file_data['media-type'] = "application/octet-stream" # TODO file_data['name'] = os.path.basename(filepath) if name is None else name file_data['size'] = os.path.getsize(filepath) desc_elt = domish.Element((NS_JINGLE_FT, 'description')) file_elt = desc_elt.addElement("file") for name in ('date', 'desc', 'media-type', 'name', 'size'): file_elt.addElement(name, content=unicode(file_data[name])) file_elt.addElement("range") # TODO file_elt.addChild(self._hash.buildHashElt()) return desc_elt def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt): """This method request confirmation for a jingle session""" content_data = session['contents'][content_name] if content_data['senders'] not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): log.warning(u"Bad sender, assuming initiator") content_data['senders'] = self._j.ROLE_INITIATOR # first we grab file informations try: file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() except StopIteration: raise failure.Failure(exceptions.DataError) file_data = {'progress_id': self._getProgressId(session, content_name)} for name in ('date', 'desc', 'media-type', 'name', 'range', 'size'): try: file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) except StopIteration: file_data[name] = '' try: hash_algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt) except exceptions.NotFound: raise failure.Failure(exceptions.DataError) if hash_algo is not None: file_data['hash_algo'] = hash_algo file_data['hash_hasher'] = hasher = self._hash.getHasher(hash_algo) file_data['data_cb'] = lambda data: hasher.update(data) try: file_data['size'] = int(file_data['size']) except ValueError: raise failure.Failure(exceptions.DataError) name = file_data['name'] if '/' in name or '\\' in name: log.warning(u"File name contain path characters, we replace them: {}".format(name)) file_data['name'] = name.replace('/', '_').replace('\\', '_') content_data['application_data']['file_data'] = file_data # now we actualy request permission to user def gotConfirmation(confirmed): if confirmed: finished_d = content_data['finished_d'] = defer.Deferred() args = [client, session, content_name, content_data] finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) return confirmed d = self._f.getDestDir(client, session['peer_jid'], content_data, file_data, stream_object=True) d.addCallback(gotConfirmation) return d def jingleHandler(self, client, action, session, content_name, desc_elt): content_data = session['contents'][content_name] application_data = content_data['application_data'] if action in (self._j.A_ACCEPTED_ACK,): pass elif action == self._j.A_SESSION_INITIATE: file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next() try: file_elt.elements(NS_JINGLE_FT, 'range').next() except StopIteration: # initiator doesn't manage , but we do so we advertise it log.debug("adding element") file_elt.addElement('range') elif action == self._j.A_SESSION_ACCEPT: assert not 'stream_object' in content_data file_data = application_data['file_data'] file_path = application_data['file_path'] size = file_data['size'] # XXX: hash security is not critical here, so we just take the higher mandatory one hasher = file_data['hash_hasher'] = self._hash.getHasher('sha-256') content_data['stream_object'] = stream.FileStreamObject( self.host, client, file_path, uid=self._getProgressId(session, content_name), size=size, data_cb=lambda data: hasher.update(data), ) finished_d = content_data['finished_d'] = defer.Deferred() args = [client, session, content_name, content_data] finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) else: log.warning(u"FIXME: unmanaged action {}".format(action)) return desc_elt def jingleSessionInfo(self, client, action, session, content_name, jingle_elt): """Called on session-info action manage checksum, and ignore element """ # TODO: manage element content_data = session['contents'][content_name] elts = [elt for elt in jingle_elt.elements() if elt.uri == NS_JINGLE_FT] if not elts: return for elt in elts: if elt.name == 'received': pass elif elt.name == 'checksum': # we have received the file hash, we need to parse it if content_data['senders'] == session['role']: log.warning(u"unexpected checksum received while we are the file sender") raise exceptions.DataError info_content_name = elt['name'] if info_content_name != content_name: # it was for an other content... return file_data = content_data['application_data']['file_data'] try: file_elt = elt.elements((NS_JINGLE_FT, 'file')).next() except StopIteration: raise exceptions.DataError algo, file_data['hash_given'] = self._hash.parseHashElt(file_elt) if algo != file_data.get('hash_algo'): log.warning(u"Hash algorithm used in given hash ({peer_algo}) doesn't correspond to the one we have used ({our_algo})" .format(peer_algo=algo, our_algo=file_data.get('hash_algo'))) else: self._receiverTryTerminate(client, session, content_name, content_data) else: raise NotImplementedError def _sendCheckSum(self, client, session, content_name, content_data): """Send the session-info with the hash checksum""" file_data = content_data['application_data']['file_data'] hasher = file_data['hash_hasher'] hash_ = hasher.hexdigest() log.debug(u"Calculated hash: {}".format(hash_)) iq_elt, jingle_elt = self._j.buildSessionInfo(client, session) checksum_elt = jingle_elt.addElement((NS_JINGLE_FT, 'checksum')) checksum_elt['creator'] = content_data['creator'] checksum_elt['name'] = content_name file_elt = checksum_elt.addElement('file') file_elt.addChild(self._hash.buildHashElt(hash_)) iq_elt.send() def _receiverTryTerminate(self, client, session, content_name, content_data, last_try=False): """Try to terminate the session This method must only be used by the receiver. It check if transfer is finished, and hash available, if everything is OK, it check hash and terminate the session @param last_try(bool): if True this mean than session must be terminated even given hash is not available @return (bool): True if session was terminated """ if not content_data.get('transfer_finished', False): return False file_data = content_data['application_data']['file_data'] hash_given = file_data.get('hash_given') if hash_given is None: if last_try: log.warning(u"sender didn't sent hash checksum, we can't check the file") self._j.delayedContentTerminate(client, session, content_name) content_data['stream_object'].close() return True return False hasher = file_data['hash_hasher'] hash_ = hasher.hexdigest() if hash_ == hash_given: log.info(u"Hash checked, file was successfully transfered: {}".format(hash_)) progress_metadata = {'hash': hash_, 'hash_algo': file_data['hash_algo'], 'hash_verified': C.BOOL_TRUE } error = None else: log.warning(u"Hash mismatch, the file was not transfered correctly") progress_metadata=None error = u"Hash mismatch: given={algo}:{given}, calculated={algo}:{our}".format( algo = file_data['hash_algo'], given = hash_given, our = hash_) self._j.delayedContentTerminate(client, session, content_name) content_data['stream_object'].close(progress_metadata, error) # we may have the last_try timer still active, so we try to cancel it try: content_data['last_try_timer'].cancel() except (KeyError, internet_error.AlreadyCalled): pass return True def _finishedCb(self, dummy, client, session, content_name, content_data): log.info(u"File transfer terminated") if content_data['senders'] != session['role']: # we terminate the session only if we are the receiver, # as recommanded in XEP-0234 §2 (after example 6) content_data['transfer_finished'] = True if not self._receiverTryTerminate(client, session, content_name, content_data): # we have not received the hash yet, we wait 5 more seconds content_data['last_try_timer'] = reactor.callLater( 5, self._receiverTryTerminate, client, session, content_name, content_data, last_try=True) else: # we are the sender, we send the checksum self._sendCheckSum(client, session, content_name, content_data) content_data['stream_object'].close() def _finishedEb(self, failure, client, session, content_name, content_data): log.warning(u"Error while streaming file: {}".format(failure)) content_data['stream_object'].close() self._j.contentTerminate(client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT) class XEP_0234_handler(XMPPHandler): implements(iwokkel.IDisco) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_JINGLE_FT)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): return [] sat-0.6.1.1+hg20180208/src/plugins/plugin_exp_jingle_stream.py0000755000175500017600000002412713243470024023702 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing pipes (experimental) # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core import exceptions from sat.core.log import getLogger log = getLogger(__name__) from sat.tools import xml_tools from sat.tools import stream from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from twisted.internet import defer from twisted.internet import protocol from twisted.internet import endpoints from twisted.internet import reactor from twisted.internet import error from twisted.internet import interfaces from zope import interface import errno NS_STREAM = 'http://salut-a-toi.org/protocol/stream' SECURITY_LIMIT=30 START_PORT = 8888 PLUGIN_INFO = { C.PI_NAME: "Jingle Stream Plugin", C.PI_IMPORT_NAME: "STREAM", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0166"], C.PI_MAIN: "JingleStream", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Jingle Stream plugin""") } CONFIRM = D_(u"{peer} wants to send you a stream, do you accept ?") CONFIRM_TITLE = D_(u"Stream Request") class StreamProtocol(protocol.Protocol): def __init__(self): self.pause = False def setPause(self, paused): # in Python 2.x, Twisted classes are old style # so we can use property and setter if paused: if not self.pause: self.transport.pauseProducing() self.pause = True else: if self.pause: self.transport.resumeProducing() self.pause = False def disconnect(self): self.transport.loseConnection() def connectionMade(self): if self.factory.client_conn is not None: self.transport.loseConnection() self.factory.setClientConn(self) def dataReceived(self, data): self.factory.writeToConsumer(data) def sendData(self, data): self.transport.write(data) def connectionLost(self, reason): if self.factory.client_conn != self: # only the first connected client_conn is relevant return if reason.type == error.ConnectionDone: self.factory.streamFinished() else: self.factory.streamFailed(reason) @interface.implementer(stream.IStreamProducer) @interface.implementer(interfaces.IPushProducer) @interface.implementer(interfaces.IConsumer) class StreamFactory(protocol.Factory): protocol = StreamProtocol consumer = None producer = None deferred = None def __init__(self): self.client_conn = None def setClientConn(self, stream_protocol): # in Python 2.x, Twisted classes are old style # so we can use property and setter assert self.client_conn is None self.client_conn = stream_protocol if self.consumer is None: self.client_conn.setPause(True) def startStream(self, consumer): if self.consumer is not None: raise exceptions.InternalError(_(u"stream can't be used with multiple consumers")) assert self.deferred is None self.consumer = consumer consumer.registerProducer(self, True) self.deferred = defer.Deferred() if self.client_conn is not None: self.client_conn.setPause(False) return self.deferred def streamFinished(self): self.client_conn = None if self.consumer: self.consumer.unregisterProducer() self.port_listening.stopListening() self.deferred.callback(None) def streamFailed(self, failure_): self.client_conn = None if self.consumer: self.consumer.unregisterProducer() self.port_listening.stopListening() self.deferred.errback(failure_) elif self.producer: self.producer.stopProducing() def stopStream(self): if self.client_conn is not None: self.client_conn.disconnect() def registerProducer(self, producer, streaming): self.producer = producer def pauseProducing(self): self.client_conn.setPause(True) def resumeProducing(self): self.client_conn.setPause(False) def stopProducing(self): if self.client_conn: self.client_conn.disconnect() def write(self, data): try: self.client_conn.sendData(data) except AttributeError: log.warning(_(u"No client connected, can't send data")) def writeToConsumer(self, data): self.consumer.write(data) class JingleStream(object): """This non standard jingle application send byte stream""" def __init__(self, host): log.info(_("Plugin Stream initialization")) self.host = host self._j = host.plugins["XEP-0166"] # shortcut to access jingle self._j.registerApplication(NS_STREAM, self) host.bridge.addMethod("streamOut", ".plugin", in_sign='ss', out_sign='s', method=self._streamOut, async=True) # jingle callbacks def _streamOut(self, to_jid_s, profile_key): client = self.host.getClient(profile_key) return self.streamOut(client, jid.JID(to_jid_s)) @defer.inlineCallbacks def streamOut(self, client, to_jid): """send a stream @param peer_jid(jid.JID): recipient @return: an unique id to identify the transfer """ port = START_PORT factory = StreamFactory() while True: endpoint = endpoints.TCP4ServerEndpoint(reactor, port) try: port_listening = yield endpoint.listen(factory) except error.CannotListenError as e: if e.socketError.errno == errno.EADDRINUSE: port += 1 else: raise e else: factory.port_listening = port_listening break self._j.initiate(client, to_jid, [{'app_ns': NS_STREAM, 'senders': self._j.ROLE_INITIATOR, 'app_kwargs': {'stream_object': factory}, }]) defer.returnValue(unicode(port)) def jingleSessionInit(self, client, session, content_name, stream_object): content_data = session['contents'][content_name] application_data = content_data['application_data'] assert 'stream_object' not in application_data application_data['stream_object'] = stream_object desc_elt = domish.Element((NS_STREAM, 'description')) return desc_elt @defer.inlineCallbacks def jingleRequestConfirmation(self, client, action, session, content_name, desc_elt): """This method request confirmation for a jingle session""" content_data = session['contents'][content_name] if content_data['senders'] not in (self._j.ROLE_INITIATOR, self._j.ROLE_RESPONDER): log.warning(u"Bad sender, assuming initiator") content_data['senders'] = self._j.ROLE_INITIATOR confirm_data = yield xml_tools.deferDialog(self.host, _(CONFIRM).format(peer=session['peer_jid'].full()), _(CONFIRM_TITLE), type_=C.XMLUI_DIALOG_CONFIRM, action_extra={'meta_from_jid': session['peer_jid'].full(), 'meta_type': "STREAM", }, security_limit=SECURITY_LIMIT, profile=client.profile) if not C.bool(confirm_data['answer']): defer.returnValue(False) try: port = int(confirm_data['port']) except (ValueError, KeyError): raise exceptions.DataError(_(u'given port is invalid')) endpoint = endpoints.TCP4ClientEndpoint(reactor, 'localhost', port) factory = StreamFactory() yield endpoint.connect(factory) content_data['stream_object'] = factory finished_d = content_data['finished_d'] = defer.Deferred() args = [client, session, content_name, content_data] finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) defer.returnValue(True) def jingleHandler(self, client, action, session, content_name, desc_elt): content_data = session['contents'][content_name] application_data = content_data['application_data'] if action in (self._j.A_ACCEPTED_ACK, self._j.A_SESSION_INITIATE): pass elif action == self._j.A_SESSION_ACCEPT: assert not 'stream_object' in content_data content_data['stream_object'] = application_data['stream_object'] finished_d = content_data['finished_d'] = defer.Deferred() args = [client, session, content_name, content_data] finished_d.addCallbacks(self._finishedCb, self._finishedEb, args, None, args) else: log.warning(u"FIXME: unmanaged action {}".format(action)) return desc_elt def _finishedCb(self, dummy, client, session, content_name, content_data): log.info(u"Pipe transfer completed") self._j.contentTerminate(client, session, content_name) content_data['stream_object'].stopStream() def _finishedEb(self, failure, client, session, content_name, content_data): log.warning(u"Error while streaming pipe: {}".format(failure)) self._j.contentTerminate(client, session, content_name, reason=self._j.REASON_FAILED_TRANSPORT) content_data['stream_object'].stopStream() sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_quiz.py0000755000175500017600000004045513243470025022211 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing Quiz game # Copyright (C) 2009-2018 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 sat.core.i18n import _ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.words.xish import domish from twisted.internet import reactor from twisted.words.protocols.jabber import client as jabber_client, jid from time import time NS_QG = 'http://www.goffi.org/protocol/quiz' QG_TAG = 'quiz' PLUGIN_INFO = { C.PI_NAME: "Quiz game plugin", C.PI_IMPORT_NAME: "Quiz", C.PI_TYPE: "Game", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0045", "XEP-0249", "ROOM-GAME"], C.PI_MAIN: "Quiz", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation of Quiz game""") } class Quiz(object): def inheritFromRoomGame(self, host): global RoomGame RoomGame = host.plugins["ROOM-GAME"].__class__ self.__class__ = type(self.__class__.__name__, (self.__class__, RoomGame, object), {}) def __init__(self, host): log.info(_("Plugin Quiz initialization")) self.inheritFromRoomGame(host) RoomGame._init_(self, host, PLUGIN_INFO, (NS_QG, QG_TAG), game_init={'stage': None}, player_init={'score': 0}) host.bridge.addMethod("quizGameLaunch", ".plugin", in_sign='asss', out_sign='', method=self._prepareRoom) # args: players, room_jid, profile host.bridge.addMethod("quizGameCreate", ".plugin", in_sign='sass', out_sign='', method=self._createGame) # args: room_jid, players, profile host.bridge.addMethod("quizGameReady", ".plugin", in_sign='sss', out_sign='', method=self._playerReady) # args: player, referee, profile host.bridge.addMethod("quizGameAnswer", ".plugin", in_sign='ssss', out_sign='', method=self.playerAnswer) host.bridge.addSignal("quizGameStarted", ".plugin", signature='ssass') # args: room_jid, referee, players, profile host.bridge.addSignal("quizGameNew", ".plugin", signature='sa{ss}s', doc={'summary': 'Start a new game', 'param_0': "room_jid: jid of game's room", 'param_1': "game_data: data of the game", 'param_2': '%(doc_profile)s'}) host.bridge.addSignal("quizGameQuestion", ".plugin", signature='sssis', doc={'summary': "Send the current question", 'param_0': "room_jid: jid of game's room", 'param_1': "question_id: question id", 'param_2': "question: question to ask", 'param_3': "timer: timer", 'param_4': '%(doc_profile)s'}) host.bridge.addSignal("quizGamePlayerBuzzed", ".plugin", signature='ssbs', doc={'summary': "A player just pressed the buzzer", 'param_0': "room_jid: jid of game's room", 'param_1': "player: player who pushed the buzzer", 'param_2': "pause: should the game be paused ?", 'param_3': '%(doc_profile)s'}) host.bridge.addSignal("quizGamePlayerSays", ".plugin", signature='sssis', doc={'summary': "A player just pressed the buzzer", 'param_0': "room_jid: jid of game's room", 'param_1': "player: player who pushed the buzzer", 'param_2': "text: what the player say", 'param_3': "delay: how long, in seconds, the text must appear", 'param_4': '%(doc_profile)s'}) host.bridge.addSignal("quizGameAnswerResult", ".plugin", signature='ssba{si}s', doc={'summary': "Result of the just given answer", 'param_0': "room_jid: jid of game's room", 'param_1': "player: player who gave the answer", 'param_2': "good_answer: True if the answer is right", 'param_3': "score: dict of score with player as key", 'param_4': '%(doc_profile)s'}) host.bridge.addSignal("quizGameTimerExpired", ".plugin", signature='ss', doc={'summary': "Nobody answered the question in time", 'param_0': "room_jid: jid of game's room", 'param_1': '%(doc_profile)s'}) host.bridge.addSignal("quizGameTimerRestarted", ".plugin", signature='sis', doc={'summary': "Nobody answered the question in time", 'param_0': "room_jid: jid of game's room", 'param_1': "time_left: time left before timer expiration", 'param_2': '%(doc_profile)s'}) def __game_data_to_xml(self, game_data): """Convert a game data dict to domish element""" game_data_elt = domish.Element((None, 'game_data')) for data in game_data: data_elt = domish.Element((None, data)) data_elt.addContent(game_data[data]) game_data_elt.addChild(data_elt) return game_data_elt def __xml_to_game_data(self, game_data_elt): """Convert a domish element with game_data to a dict""" game_data = {} for data_elt in game_data_elt.elements(): game_data[data_elt.name] = unicode(data_elt) return game_data def __answer_result_to_signal_args(self, answer_result_elt): """Parse answer result element and return a tuple of signal arguments @param answer_result_elt: answer result element @return: (player, good_answer, score)""" score = {} for score_elt in answer_result_elt.elements(): score[score_elt['player']] = int(score_elt['score']) return (answer_result_elt['player'], answer_result_elt['good_answer'] == str(True), score) def __answer_result(self, player_answering, good_answer, game_data): """Convert a domish an answer_result element @param player_answering: player who gave the answer @param good_answer: True is the answer is right @param game_data: data of the game""" players_data = game_data['players_data'] score = {} for player in game_data['players']: score[player] = players_data[player]['score'] answer_result_elt = domish.Element((None, 'answer_result')) answer_result_elt['player'] = player_answering answer_result_elt['good_answer'] = str(good_answer) for player in score: score_elt = domish.Element((None, "score")) score_elt['player'] = player score_elt['score'] = str(score[player]) answer_result_elt.addChild(score_elt) return answer_result_elt def __ask_question(self, question_id, question, timer): """Create a element for asking a question""" question_elt = domish.Element((None, 'question')) question_elt['id'] = question_id question_elt['timer'] = str(timer) question_elt.addContent(question) return question_elt def __start_play(self, room_jid, game_data, profile): """Start the game (tell to the first player after dealer to play""" client = self.host.getClient(profile) game_data['stage'] = "play" next_player_idx = game_data['current_player'] = (game_data['init_player'] + 1) % len(game_data['players']) # the player after the dealer start game_data['first_player'] = next_player = game_data['players'][next_player_idx] to_jid = jid.JID(room_jid.userhost() + "/" + next_player) mess = self.createGameElt(to_jid) mess.firstChildElement().addElement('your_turn') client.send(mess) def playerAnswer(self, player, referee, answer, profile_key=C.PROF_KEY_NONE): """Called when a player give an answer""" client = self.host.getClient(profile_key) log.debug(u'new player answer (%(profile)s): %(answer)s' % {'profile': client.profile, 'answer': answer}) mess = self.createGameElt(jid.JID(referee)) answer_elt = mess.firstChildElement().addElement('player_answer') answer_elt['player'] = player answer_elt.addContent(answer) client.send(mess) def timerExpired(self, room_jid, profile): """Called when nobody answered the question in time""" client = self.host.getClient(profile) game_data = self.games[room_jid] game_data['stage'] = 'expired' mess = self.createGameElt(room_jid) mess.firstChildElement().addElement('timer_expired') client.send(mess) reactor.callLater(4, self.askQuestion, room_jid, client.profile) def pauseTimer(self, room_jid): """Stop the timer and save the time left""" game_data = self.games[room_jid] left = max(0, game_data["timer"].getTime() - time()) game_data['timer'].cancel() game_data['time_left'] = int(left) game_data['previous_stage'] = game_data['stage'] game_data['stage'] = "paused" def restartTimer(self, room_jid, profile): """Restart a timer with the saved time""" client = self.host.getClient(profile) game_data = self.games[room_jid] assert game_data['time_left'] is not None mess = self.createGameElt(room_jid) mess.firstChildElement().addElement('timer_restarted') jabber_client.restarted_elt["time_left"] = str(game_data['time_left']) client.send(mess) game_data["timer"] = reactor.callLater(game_data['time_left'], self.timerExpired, room_jid, profile) game_data["time_left"] = None game_data['stage'] = game_data['previous_stage'] del game_data['previous_stage'] def askQuestion(self, room_jid, profile): """Ask a new question""" client = self.host.getClient(profile) game_data = self.games[room_jid] game_data['stage'] = "question" game_data['question_id'] = "1" timer = 30 mess = self.createGameElt(room_jid) mess.firstChildElement().addChild(self.__ask_question(game_data['question_id'], u"Quel est l'âge du capitaine ?", timer)) client.send(mess) game_data["timer"] = reactor.callLater(timer, self.timerExpired, room_jid, profile) game_data["time_left"] = None def checkAnswer(self, room_jid, player, answer, profile): """Check if the answer given is right""" client = self.host.getClient(profile) game_data = self.games[room_jid] players_data = game_data['players_data'] good_answer = game_data['question_id'] == "1" and answer == "42" players_data[player]['score'] += 1 if good_answer else -1 players_data[player]['score'] = min(9, max(0, players_data[player]['score'])) mess = self.createGameElt(room_jid) mess.firstChildElement().addChild(self.__answer_result(player, good_answer, game_data)) client.send(mess) if good_answer: reactor.callLater(4, self.askQuestion, room_jid, profile) else: reactor.callLater(4, self.restartTimer, room_jid, profile) def newGame(self, room_jid, profile): """Launch a new round""" common_data = {'game_score': 0} new_game_data = {"instructions": _(u"""Bienvenue dans cette partie rapide de quizz, le premier à atteindre le score de 9 remporte le jeu Attention, tu es prêt ?""")} msg_elts = self.__game_data_to_xml(new_game_data) RoomGame.newRound(self, room_jid, (common_data, msg_elts), profile) reactor.callLater(10, self.askQuestion, room_jid, profile) def room_game_cmd(self, mess_elt, profile): client = self.host.getClient(profile) from_jid = jid.JID(mess_elt['from']) room_jid = jid.JID(from_jid.userhost()) game_elt = mess_elt.firstChildElement() game_data = self.games[room_jid] # if 'players_data' in game_data: #  players_data = game_data['players_data'] for elt in game_elt.elements(): if elt.name == 'started': # new game created players = [] for player in elt.elements(): players.append(unicode(player)) self.host.bridge.quizGameStarted(room_jid.userhost(), from_jid.full(), players, profile) elif elt.name == 'player_ready': # ready to play player = elt['player'] status = self.games[room_jid]['status'] nb_players = len(self.games[room_jid]['players']) status[player] = 'ready' log.debug(_(u'Player %(player)s is ready to start [status: %(status)s]') % {'player': player, 'status': status}) if status.values().count('ready') == nb_players: # everybody is ready, we can start the game self.newGame(room_jid, profile) elif elt.name == 'game_data': self.host.bridge.quizGameNew(room_jid.userhost(), self.__xml_to_game_data(elt), profile) elif elt.name == 'question': # A question is asked self.host.bridge.quizGameQuestion(room_jid.userhost(), elt["id"], unicode(elt), int(elt["timer"]), profile) elif elt.name == 'player_answer': player = elt['player'] pause = game_data['stage'] == 'question' # we pause the game only if we are have a question at the moment # we first send a buzzer message mess = self.createGameElt(room_jid) buzzer_elt = mess.firstChildElement().addElement('player_buzzed') buzzer_elt['player'] = player buzzer_elt['pause'] = str(pause) client.send(mess) if pause: self.pauseTimer(room_jid) # and we send the player answer mess = self.createGameElt(room_jid) _answer = unicode(elt) say_elt = mess.firstChildElement().addElement('player_says') say_elt['player'] = player say_elt.addContent(_answer) say_elt['delay'] = "3" reactor.callLater(2, client.send, mess) reactor.callLater(6, self.checkAnswer, room_jid, player, _answer, profile=profile) elif elt.name == 'player_buzzed': self.host.bridge.quizGamePlayerBuzzed(room_jid.userhost(), elt["player"], elt['pause'] == str(True), profile) elif elt.name == 'player_says': self.host.bridge.quizGamePlayerSays(room_jid.userhost(), elt["player"], unicode(elt), int(elt["delay"]), profile) elif elt.name == 'answer_result': player, good_answer, score = self.__answer_result_to_signal_args(elt) self.host.bridge.quizGameAnswerResult(room_jid.userhost(), player, good_answer, score, profile) elif elt.name == 'timer_expired': self.host.bridge.quizGameTimerExpired(room_jid.userhost(), profile) elif elt.name == 'timer_restarted': self.host.bridge.quizGameTimerRestarted(room_jid.userhost(), int(elt['time_left']), profile) else: log.error(_(u'Unmanaged game element: %s') % elt.name) sat-0.6.1.1+hg20180208/src/plugins/plugin_blog_import_dokuwiki.py0000755000175500017600000003551613243470024024432 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT plugin to import dokuwiki blogs # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from sat.tools import xml_tools from twisted.internet import threads from collections import OrderedDict import calendar import urllib import urlparse import tempfile import re import time import os.path try: from dokuwiki import DokuWiki, DokuWikiError # this is a new dependency except ImportError: raise exceptions.MissingModule(u'Missing module dokuwiki, please install it with "pip install dokuwiki"') try: from PIL import Image # this is already needed by plugin XEP-0054 except: raise exceptions.MissingModule(u"Missing module pillow, please download/install it from https://python-pillow.github.io") PLUGIN_INFO = { C.PI_NAME: "Dokuwiki import", C.PI_IMPORT_NAME: "IMPORT_DOKUWIKI", C.PI_TYPE: C.PLUG_TYPE_BLOG, C.PI_DEPENDENCIES: ["BLOG_IMPORT"], C.PI_MAIN: "DokuwikiImport", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Blog importer for Dokuwiki blog engine.""") } SHORT_DESC = D_(u"import posts from Dokuwiki blog engine") LONG_DESC = D_(u"""This importer handle Dokuwiki blog engine. To use it, you need an admin access to a running Dokuwiki website (local or on the Internet). The importer retrieves the data using the XMLRPC Dokuwiki API. You can specify a namespace (that could be a namespace directory or a single post) or leave it empty to use the root namespace "/" and import all the posts. You can specify a new media repository to modify the internal media links and make them point to the URL of your choice, but note that the upload is not done automatically: a temporary directory will be created on your local drive and you will need to upload it yourself to your repository via SSH or FTP. Following options are recognized: location: DokuWiki site URL user: DokuWiki admin user passwd: DokuWiki admin password namespace: DokuWiki namespace to import (default: root namespace "/") media_repo: URL to the new remote media repository (default: none) limit: maximal number of posts to import (default: 100) Example of usage (with jp frontend): jp import dokuwiki -p dave --pwd xxxxxx --connect http://127.0.1.1 -o user souliane -o passwd qwertz -o namespace public:2015:10 -o media_repo http://media.diekulturvermittlung.at This retrieves the 100 last blog posts from http://127.0.1.1 that are inside the namespace "public:2015:10" using the Dokuwiki user "souliane", and it imports them to sat profile dave's microblog node. Internal Dokuwiki media that were hosted on http://127.0.1.1 are now pointing to http://media.diekulturvermittlung.at. """) DEFAULT_MEDIA_REPO = "" DEFAULT_NAMESPACE = "/" DEFAULT_LIMIT = 100 # you might get a DBUS timeout (no reply) if it lasts too long class Importer(DokuWiki): def __init__(self, url, user, passwd, media_repo=DEFAULT_MEDIA_REPO, limit=DEFAULT_LIMIT): """ @param url (unicode): DokuWiki site URL @param user (unicode): DokuWiki admin user @param passwd (unicode): DokuWiki admin password @param media_repo (unicode): New remote media repository """ DokuWiki.__init__(self, url, user, passwd) self.url = url self.media_repo = media_repo self.temp_dir = tempfile.mkdtemp() if self.media_repo else None self.limit = limit self.posts_data = OrderedDict() def getPostId(self, post): """Return a unique and constant post id @param post(dict): parsed post data @return (unicode): post unique item id """ return unicode(post['id']) def getPostUpdated(self, post): """Return the update date. @param post(dict): parsed post data @return (unicode): update date """ return unicode(post['mtime']) def getPostPublished(self, post): """Try to parse the date from the message ID, else use "mtime". The date can be extracted if the message ID looks like one of: - namespace:YYMMDD_short_title - namespace:YYYYMMDD_short_title @param post (dict): parsed post data @return (unicode): publication date """ id_, default = unicode(post["id"]), unicode(post["mtime"]) try: date = id_.split(":")[-1].split("_")[0] except KeyError: return default try: time_struct = time.strptime(date, "%y%m%d") except ValueError: try: time_struct = time.strptime(date, "%Y%m%d") except ValueError: return default return unicode(calendar.timegm(time_struct)) def processPost(self, post, profile_jid): """Process a single page. @param post (dict): parsed post data @param profile_jid """ # get main information id_ = self.getPostId(post) updated = self.getPostUpdated(post) published = self.getPostPublished(post) # manage links backlinks = self.pages.backlinks(id_) for link in self.pages.links(id_): if link["type"] != "extern": assert link["type"] == "local" page = link["page"] backlinks.append(page[1:] if page.startswith(":") else page) self.pages.get(id_) content_xhtml = self.processContent(self.pages.html(id_), backlinks, profile_jid) # XXX: title is already in content_xhtml and difficult to remove, so leave it # title = content.split("\n")[0].strip(u"\ufeff= ") # build the extra data dictionary mb_data = {"id": id_, "published": published, "updated": updated, "author": profile_jid.user, # "content": content, # when passed, it is displayed in Libervia instead of content_xhtml "content_xhtml": content_xhtml, # "title": title, "allow_comments": "true", } # find out if the message access is public or restricted namespace = id_.split(":")[0] if namespace and namespace.lower() not in ("public", "/"): mb_data["group"] = namespace # roster group must exist self.posts_data[id_] = {'blog': mb_data, 'comments':[[]]} def process(self, client, namespace=DEFAULT_NAMESPACE): """Process a namespace or a single page. @param namespace (unicode): DokuWiki namespace (or page) to import """ profile_jid = client.jid log.info("Importing data from DokuWiki %s" % self.version) try: pages_list = self.pages.list(namespace) except DokuWikiError: log.warning('Could not list Dokuwiki pages: please turn the "display_errors" setting to "Off" in the php.ini of the webserver hosting DokuWiki.') return if not pages_list: # namespace is actually a page? names = namespace.split(":") real_namespace = ":".join(names[0:-1]) pages_list = self.pages.list(real_namespace) pages_list = [page for page in pages_list if page["id"] == namespace] namespace = real_namespace count = 0 for page in pages_list: self.processPost(page, profile_jid) count += 1 if count >= self.limit : break return (self.posts_data.itervalues(), len(self.posts_data)) def processContent(self, text, backlinks, profile_jid): """Do text substitutions and file copy. @param text (unicode): message content @param backlinks (list[unicode]): list of backlinks """ text = text.strip(u"\ufeff") # this is at the beginning of the file (BOM) for backlink in backlinks: src = '/doku.php?id=%s"' % backlink tgt = '/blog/%s/%s" target="#"' % (profile_jid.user, backlink) text = text.replace(src, tgt) subs = {} link_pattern = r"""<(img|a)[^>]* (src|href)="([^"]+)"[^>]*>""" for tag in re.finditer(link_pattern, text): type_, attr, link = tag.group(1), tag.group(2), tag.group(3) assert (type_ == "img" and attr == "src") or (type_ == "a" and attr == "href") if re.match(r"^\w*://", link): # absolute URL to link directly continue if self.media_repo: self.moveMedia(link, subs) elif link not in subs: subs[link] = urlparse.urljoin(self.url, link) for url, new_url in subs.iteritems(): text = text.replace(url, new_url) return text def moveMedia(self, link, subs): """Move a media from the DokuWiki host to the new repository. This also updates the hyperlinks to internal media files. @param link (unicode): media link @param subs (dict): substitutions data """ url = urlparse.urljoin(self.url, link) user_media = re.match(r"(/lib/exe/\w+.php\?)(.*)", link) thumb_width = None if user_media: # media that has been added by the user params = urlparse.parse_qs(urlparse.urlparse(url).query) try: media = params["media"][0] except KeyError: log.warning("No media found in fetch URL: %s" % user_media.group(2)) return if re.match(r"^\w*://", media): # external URL to link directly subs[link] = media return try: # create thumbnail thumb_width = params["w"][0] except KeyError: pass filename = media.replace(":", "/") # XXX: avoid "precondition failed" error (only keep the media parameter) url = urlparse.urljoin(self.url, "/lib/exe/fetch.php?media=%s" % media) elif link.startswith("/lib/plugins/"): # other link added by a plugin or something else filename = link[13:] else: # fake alert... there's no media (or we don't handle it yet) return filepath = os.path.join(self.temp_dir, filename) self.downloadMedia(url, filepath) if thumb_width: filename = os.path.join("thumbs", thumb_width, filename) thumbnail = os.path.join(self.temp_dir, filename) self.createThumbnail(filepath, thumbnail, thumb_width) new_url = os.path.join(self.media_repo, filename) subs[link] = new_url def downloadMedia(self, source, dest): """Copy media to localhost. @param source (unicode): source url @param dest (unicode): target path """ dirname = os.path.dirname(dest) if not os.path.exists(dest): if not os.path.exists(dirname): os.makedirs(dirname) urllib.urlretrieve(source, dest) log.debug("DokuWiki media file copied to %s" % dest) def createThumbnail(self, source, dest, width): """Create a thumbnail. @param source (unicode): source file path @param dest (unicode): destination file path @param width (unicode): thumbnail's width """ thumb_dir = os.path.dirname(dest) if not os.path.exists(thumb_dir): os.makedirs(thumb_dir) try: im = Image.open(source) im.thumbnail((width, int(width) * im.size[0] / im.size[1])) im.save(dest) log.debug("DokuWiki media thumbnail created: %s" % dest) except IOError: log.error("Cannot create DokuWiki media thumbnail %s" % dest) class DokuwikiImport(object): def __init__(self, host): log.info(_("plugin Dokuwiki Import initialization")) self.host = host self._blog_import = host.plugins['BLOG_IMPORT'] self._blog_import.register('dokuwiki', self.DkImport, SHORT_DESC, LONG_DESC) def DkImport(self, client, location, options=None): """Import from DokuWiki to PubSub @param location (unicode): DokuWiki site URL @param options (dict, None): DokuWiki import parameters - user (unicode): DokuWiki admin user - passwd (unicode): DokuWiki admin password - namespace (unicode): DokuWiki namespace to import - media_repo (unicode): New remote media repository """ options[self._blog_import.OPT_HOST] = location try: user = options["user"] except KeyError: raise exceptions.DataError('parameter "user" is required') try: passwd = options["passwd"] except KeyError: raise exceptions.DataError('parameter "passwd" is required') opt_upload_images = options.get(self._blog_import.OPT_UPLOAD_IMAGES, None) try: media_repo = options["media_repo"] if opt_upload_images: options[self._blog_import.OPT_UPLOAD_IMAGES] = False # force using --no-images-upload info_msg = _("DokuWiki media files will be *downloaded* to {temp_dir} - to finish the import you have to upload them *manually* to {media_repo}") except KeyError: media_repo = DEFAULT_MEDIA_REPO if opt_upload_images: info_msg = _("DokuWiki media files will be *uploaded* to the XMPP server. Hyperlinks to these media may not been updated though.") else: info_msg = _("DokuWiki media files will *stay* on {location} - some of them may be protected by DokuWiki ACL and will not be accessible.") try: namespace = options["namespace"] except KeyError: namespace = DEFAULT_NAMESPACE try: limit = options["limit"] except KeyError: limit = DEFAULT_LIMIT dk_importer = Importer(location, user, passwd, media_repo, limit) info_msg = info_msg.format(temp_dir=dk_importer.temp_dir, media_repo=media_repo, location=location) self.host.actionNew({'xmlui': xml_tools.note(info_msg).toXml()}, profile=client.profile) d = threads.deferToThread(dk_importer.process, client, namespace) return d sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_register_account.py0000644000175500017600000001250513243470025024551 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SàT plugin for registering a new XMPP account # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.log import getLogger log = getLogger(__name__) from sat.core.constants import Const as C from twisted.words.protocols.jabber import jid from sat.memory.memory import Sessions from sat.tools import xml_tools from sat.tools.xml_tools import SAT_FORM_PREFIX, SAT_PARAM_SEPARATOR PLUGIN_INFO = { C.PI_NAME: "Register Account Plugin", C.PI_IMPORT_NAME: "REGISTER-ACCOUNT", C.PI_TYPE: "MISC", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0077"], C.PI_RECOMMENDATIONS: [], C.PI_MAIN: "RegisterAccount", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _(u"""Register XMPP account""") } class RegisterAccount(object): # FIXME: this plugin is messy and difficult to read, it needs to be cleaned up and documented def __init__(self, host): log.info(_(u"Plugin Register Account initialization")) self.host = host self._sessions = Sessions() host.registerCallback(self.registerNewAccountCB, with_data=True, force_id="registerNewAccount") self.__register_account_id = host.registerCallback(self._registerConfirmation, with_data=True) def registerNewAccountCB(self, data, profile): """Called when the user click on the "New account" button.""" session_data = {} # FIXME: following loop is overcomplicated, hard to read # FIXME: while used with parameters, hashed password is used and overwrite clear one for param in (u'JabberID', u'Password', C.FORCE_PORT_PARAM, C.FORCE_SERVER_PARAM): try: session_data[param] = data[SAT_FORM_PREFIX + u"Connection" + SAT_PARAM_SEPARATOR + param] except KeyError: if param in (C.FORCE_PORT_PARAM, C.FORCE_SERVER_PARAM): session_data[param] = '' for param in (u'JabberID', u'Password'): if not session_data[param]: form_ui = xml_tools.XMLUI(u"popup", title=D_(u"Missing values")) form_ui.addText(D_(u"No user JID or password given: can't register new account.")) return {u'xmlui': form_ui.toXml()} session_data['user'], host, resource = jid.parse(session_data['JabberID']) session_data['server'] = session_data[C.FORCE_SERVER_PARAM] or host session_id, dummy = self._sessions.newSession(session_data, profile=profile) form_ui = xml_tools.XMLUI("form", title=D_("Register new account"), submit_id=self.__register_account_id, session_id=session_id) form_ui.addText(D_(u"Do you want to register a new XMPP account {jid}?").format( jid = session_data['JabberID'])) return {'xmlui': form_ui.toXml()} def _registerConfirmation(self, data, profile): """Save the related parameters and proceed the registration.""" session_data = self._sessions.profileGet(data['session_id'], profile) self.host.memory.setParam("JabberID", session_data["JabberID"], "Connection", profile_key=profile) self.host.memory.setParam("Password", session_data["Password"], "Connection", profile_key=profile) self.host.memory.setParam(C.FORCE_SERVER_PARAM, session_data[C.FORCE_SERVER_PARAM], "Connection", profile_key=profile) self.host.memory.setParam(C.FORCE_PORT_PARAM, session_data[C.FORCE_PORT_PARAM], "Connection", profile_key=profile) d = self._registerNewAccount(jid.JID(session_data['JabberID']), session_data["Password"], None, session_data['server']) del self._sessions[data['session_id']] return d def _registerNewAccount(self, client, jid_, password, email, server): # FIXME: port is not set here def registeredCb(dummy): xmlui = xml_tools.XMLUI(u"popup", title=D_(u"Confirmation")) xmlui.addText(D_("Registration successful.")) return ({'xmlui': xmlui.toXml()}) def registeredEb(failure): xmlui = xml_tools.XMLUI("popup", title=D_("Failure")) xmlui.addText(D_("Registration failed: %s") % failure.getErrorMessage()) try: if failure.value.condition == 'conflict': xmlui.addText(D_("Username already exists, please choose an other one.")) except AttributeError: pass return ({'xmlui': xmlui.toXml()}) registered_d = self.host.plugins['XEP-0077'].registerNewAccount(client, jid_, password, email=email, host=server, port=C.XMPP_C2S_PORT) registered_d.addCallbacks(registeredCb, registeredEb) return registered_d sat-0.6.1.1+hg20180208/src/plugins/plugin_xep_0096.py0000755000175500017600000003520213243470025021452 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing xep-0096 # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from sat.tools import xml_tools from sat.tools import stream from twisted.words.xish import domish from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import error import os NS_SI_FT = "http://jabber.org/protocol/si/profile/file-transfer" IQ_SET = '/iq[@type="set"]' SI_PROFILE_NAME = "file-transfer" SI_PROFILE = "http://jabber.org/protocol/si/profile/" + SI_PROFILE_NAME PLUGIN_INFO = { C.PI_NAME: "XEP-0096 Plugin", C.PI_IMPORT_NAME: "XEP-0096", C.PI_TYPE: "XEP", C.PI_PROTOCOLS: ["XEP-0096"], C.PI_DEPENDENCIES: ["XEP-0020", "XEP-0095", "XEP-0065", "XEP-0047", "FILE"], C.PI_MAIN: "XEP_0096", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Implementation of SI File Transfer""") } class XEP_0096(object): # TODO: call self._f.unregister when unloading order will be managing (i.e. when depenencies will be unloaded at the end) def __init__(self, host): log.info(_("Plugin XEP_0096 initialization")) self.host = host self.managed_stream_m = [self.host.plugins["XEP-0065"].NAMESPACE, self.host.plugins["XEP-0047"].NAMESPACE] # Stream methods managed self._f = self.host.plugins["FILE"] self._f.register(NS_SI_FT, self.sendFile, priority=0, method_name=u"Stream Initiation") self._si = self.host.plugins["XEP-0095"] self._si.registerSIProfile(SI_PROFILE_NAME, self._transferRequest) host.bridge.addMethod("siSendFile", ".plugin", in_sign='sssss', out_sign='s', method=self._sendFile) def unload(self): self._si.unregisterSIProfile(SI_PROFILE_NAME) def _badRequest(self, client, iq_elt, message=None): """Send a bad-request error @param iq_elt(domish.Element): initial element of the SI request @param message(None, unicode): informational message to display in the logs """ if message is not None: log.warning(message) self._si.sendError(client, iq_elt, 'bad-request') def _parseRange(self, parent_elt, file_size): """find and parse element @param parent_elt(domish.Element): direct parent of the element @return (tuple[bool, int, int]): a tuple with - True if range is required - range_offset - range_length """ try: range_elt = parent_elt.elements(NS_SI_FT, 'range').next() except StopIteration: range_ = False range_offset = None range_length = None else: range_ = True try: range_offset = int(range_elt['offset']) except KeyError: range_offset = 0 try: range_length = int(range_elt['length']) except KeyError: range_length = file_size if range_offset != 0 or range_length != file_size: raise NotImplementedError # FIXME return range_, range_offset, range_length def _transferRequest(self, client, iq_elt, si_id, si_mime_type, si_elt): """Called when a file transfer is requested @param iq_elt(domish.Element): initial element of the SI request @param si_id(unicode): Stream Initiation session id @param si_mime_type("unicode"): Mime type of the file (or default "application/octet-stream" if unknown) @param si_elt(domish.Element): request """ log.info(_("XEP-0096 file transfer requested")) peer_jid = jid.JID(iq_elt['from']) try: file_elt = si_elt.elements(NS_SI_FT, "file").next() except StopIteration: return self._badRequest(client, iq_elt, "No element found in SI File Transfer request") try: feature_elt = self.host.plugins["XEP-0020"].getFeatureElt(si_elt) except exceptions.NotFound: return self._badRequest(client, iq_elt, "No element found in SI File Transfer request") try: filename = file_elt["name"] file_size = int(file_elt["size"]) except (KeyError, ValueError): return self._badRequest(client, iq_elt, "Malformed SI File Transfer request") file_date = file_elt.getAttribute("date") file_hash = file_elt.getAttribute("hash") log.info(u"File proposed: name=[{name}] size={size}".format(name=filename, size=file_size)) try: file_desc = unicode(file_elt.elements(NS_SI_FT, 'desc').next()) except StopIteration: file_desc = '' try: range_, range_offset, range_length = self._parseRange(file_elt, file_size) except ValueError: return self._badRequest(client, iq_elt, "Malformed SI File Transfer request") try: stream_method = self.host.plugins["XEP-0020"].negotiate(feature_elt, 'stream-method', self.managed_stream_m, namespace=None) except KeyError: return self._badRequest(client, iq_elt, "No stream method found") if stream_method: if stream_method == self.host.plugins["XEP-0065"].NAMESPACE: plugin = self.host.plugins["XEP-0065"] elif stream_method == self.host.plugins["XEP-0047"].NAMESPACE: plugin = self.host.plugins["XEP-0047"] else: log.error(u"Unknown stream method, this should not happen at this stage, cancelling transfer") else: log.warning(u"Can't find a valid stream method") self._si.sendError(client, iq_elt, 'not-acceptable') return #if we are here, the transfer can start, we just need user's agreement data = {"name": filename, "peer_jid": peer_jid, "size": file_size, "date": file_date, "hash": file_hash, "desc": file_desc, "range": range_, "range_offset": range_offset, "range_length": range_length, "si_id": si_id, "progress_id": si_id, "stream_method": stream_method, "stream_plugin": plugin} d = self._f.getDestDir(client, peer_jid, data, data, stream_object=True) d.addCallback(self.confirmationCb, client, iq_elt, data) def confirmationCb(self, accepted, client, iq_elt, data): """Called on confirmation answer @param accepted(bool): True if file transfer is accepted @param iq_elt(domish.Element): initial SI request @param data(dict): session data """ if not accepted: log.info(u"File transfer declined") self._si.sendError(client, iq_elt, 'forbidden') return # data, timeout, stream_method, failed_methods = client._xep_0096_waiting_for_approval[sid] # can_range = data['can_range'] == "True" # range_offset = 0 # if timeout.active(): # timeout.cancel() # try: # dest_path = frontend_data['dest_path'] # except KeyError: # log.error(_('dest path not found in frontend_data')) # del client._xep_0096_waiting_for_approval[sid] # return # if stream_method == self.host.plugins["XEP-0065"].NAMESPACE: # plugin = self.host.plugins["XEP-0065"] # elif stream_method == self.host.plugins["XEP-0047"].NAMESPACE: # plugin = self.host.plugins["XEP-0047"] # else: # log.error(_("Unknown stream method, this should not happen at this stage, cancelling transfer")) # del client._xep_0096_waiting_for_approval[sid] # return # file_obj = self._getFileObject(dest_path, can_range) # range_offset = file_obj.tell() d = data['stream_plugin'].createSession(client, data['stream_object'], data['peer_jid'], data['si_id']) d.addCallback(self._transferCb, client, data) d.addErrback(self._transferEb, client, data) #we can send the iq result feature_elt = self.host.plugins["XEP-0020"].chooseOption({'stream-method': data['stream_method']}, namespace=None) misc_elts = [] misc_elts.append(domish.Element((SI_PROFILE, "file"))) # if can_range: # range_elt = domish.Element((None, "range")) # range_elt['offset'] = str(range_offset) # #TODO: manage range length # misc_elts.append(range_elt) self._si.acceptStream(client, iq_elt, feature_elt, misc_elts) def _transferCb(self, dummy, client, data): """Called by the stream method when transfer successfuly finished @param data: session data """ #TODO: check hash data['stream_object'].close() log.info(u'Transfer {si_id} successfuly finished'.format(**data)) def _transferEb(self, failure, client, data): """Called when something went wrong with the transfer @param id: stream id @param data: session data """ log.warning(u'Transfer {si_id} failed: {reason}'.format(reason=unicode(failure.value), **data)) data['stream_object'].close() def _sendFile(self, peer_jid_s, filepath, name, desc, profile=C.PROF_KEY_NONE): client = self.host.getClient(profile) return self.sendFile(client, jid.JID(peer_jid_s), filepath, name or None, desc or None) def sendFile(self, client, peer_jid, filepath, name=None, desc=None): """Send a file using XEP-0096 @param peer_jid(jid.JID): recipient @param filepath(str): absolute path to the file to send @param name(unicode): name of the file to send name must not contain "/" characters @param desc: description of the file @param profile: %(doc_profile)s @return: an unique id to identify the transfer """ feature_elt = self.host.plugins["XEP-0020"].proposeFeatures({'stream-method': self.managed_stream_m}, namespace=None) file_transfer_elts = [] statinfo = os.stat(filepath) file_elt = domish.Element((SI_PROFILE, 'file')) file_elt['name'] = name or os.path.basename(filepath) assert '/' not in file_elt['name'] size = statinfo.st_size file_elt['size'] = str(size) if desc: file_elt.addElement('desc', content=desc) file_transfer_elts.append(file_elt) file_transfer_elts.append(domish.Element((None, 'range'))) sid, offer_d = self._si.proposeStream(client, peer_jid, SI_PROFILE, feature_elt, file_transfer_elts) args = [filepath, sid, size, client] offer_d.addCallbacks(self._fileCb, self._fileEb, args, None, args) return sid def _fileCb(self, result_tuple, filepath, sid, size, client): iq_elt, si_elt = result_tuple try: feature_elt = self.host.plugins["XEP-0020"].getFeatureElt(si_elt) except exceptions.NotFound: log.warning(u"No element found in result while expected") return choosed_options = self.host.plugins["XEP-0020"].getChoosedOptions(feature_elt, namespace=None) try: stream_method = choosed_options["stream-method"] except KeyError: log.warning(u"No stream method choosed") return try: file_elt = si_elt.elements(NS_SI_FT, "file").next() except StopIteration: pass else: range_, range_offset, range_length = self._parseRange(file_elt, size) if stream_method == self.host.plugins["XEP-0065"].NAMESPACE: plugin = self.host.plugins["XEP-0065"] elif stream_method == self.host.plugins["XEP-0047"].NAMESPACE: plugin = self.host.plugins["XEP-0047"] else: log.warning(u"Invalid stream method received") return stream_object = stream.FileStreamObject(self.host, client, filepath, uid=sid, size=size, ) d = plugin.startStream(client, stream_object, jid.JID(iq_elt['from']), sid) d.addCallback(self._sendCb, client, sid, stream_object) d.addErrback(self._sendEb, client, sid, stream_object) def _fileEb(self, failure, filepath, sid, size, client): if failure.check(error.StanzaError): stanza_err = failure.value if stanza_err.code == '403' and stanza_err.condition == 'forbidden': from_s = stanza_err.stanza['from'] log.info(u"File transfer refused by {}".format(from_s)) msg = D_(u"The contact {} has refused your file").format(from_s) title = D_(u"File refused") xml_tools.quickNote(self.host, client, msg, title, C.XMLUI_DATA_LVL_INFO) else: log.warning(_(u"Error during file transfer")) msg = D_(u"Something went wrong during the file transfer session initialisation: {reason}").format(reason=unicode(stanza_err)) title = D_(u"File transfer error") xml_tools.quickNote(self.host, client, msg, title, C.XMLUI_DATA_LVL_ERROR) elif failure.check(exceptions.DataError): log.warning(u'Invalid stanza received') else: log.error(u'Error while proposing stream: {}'.format(failure)) def _sendCb(self, dummy, client, sid, stream_object): log.info(_(u'transfer {sid} successfuly finished [{profile}]').format( sid=sid, profile=client.profile)) stream_object.close() def _sendEb(self, failure, client, sid, stream_object): log.warning(_(u'transfer {sid} failed [{profile}]: {reason}').format( sid=sid, profile=client.profile, reason=unicode(failure.value), )) stream_object.close() sat-0.6.1.1+hg20180208/src/plugins/plugin_tmp_directory_subscription.py0000644000175500017600000000515313243470025025667 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for directory subscription # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Jérôme Poisson (goffi@goffi.org) # Copyright (C) 2015, 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) PLUGIN_INFO = { C.PI_NAME: "Directory subscription plugin", C.PI_IMPORT_NAME: "DIRECTORY-SUBSCRIPTION", C.PI_TYPE: "TMP", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: ["XEP-0050", "XEP-0055"], C.PI_RECOMMENDATIONS: [], C.PI_MAIN: "DirectorySubscription", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Implementation of directory subscription""") } NS_COMMANDS = "http://jabber.org/protocol/commands" CMD_UPDATE_SUBSCRIBTION = "update" class DirectorySubscription(object): def __init__(self, host): log.info(_("Directory subscription plugin initialization")) self.host = host host.importMenu((D_("Service"), D_("Directory subscription")), self.subscribe, security_limit=1, help_string=D_("User directory subscription")) def subscribe(self, raw_data, profile): """Request available commands on the jabber search service associated to profile's host. @param raw_data (dict): data received from the frontend @param profile (unicode): %(doc_profile)s @return: a deferred dict{unicode: unicode} """ d = self.host.plugins["XEP-0055"]._getHostServices(profile) def got_services(services): service_jid = services[0] session_id, session_data = self.host.plugins["XEP-0050"].requesting.newSession(profile=profile) session_data["jid"] = service_jid session_data["node"] = CMD_UPDATE_SUBSCRIBTION data = {"session_id": session_id} return self.host.plugins["XEP-0050"]._requestingEntity(data, profile) return d.addCallback(got_services) sat-0.6.1.1+hg20180208/src/plugins/plugin_exp_lang_detect.py0000755000175500017600000000615113243470024023325 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin to detect language (experimental) # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions try: from langid.langid import LanguageIdentifier, model except ImportError: raise exceptions.MissingModule(u'Missing module langid, please download/install it with "pip install langid")') identifier = LanguageIdentifier.from_modelstring(model, norm_probs=False) PLUGIN_INFO = { C.PI_NAME: "Language detection plugin", C.PI_IMPORT_NAME: "EXP-LANG-DETECT", C.PI_TYPE: "EXP", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [], C.PI_MAIN: "LangDetect", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Detect and set message language when unknown""") } CATEGORY = D_(u"Misc") NAME = u"lang_detect" LABEL = D_(u"language detection") PARAMS = """ """.format(category_name=CATEGORY, name=NAME, label=_(LABEL), ) class LangDetect(object): def __init__(self, host): log.info(_(u"Language detection plugin initialization")) self.host = host host.memory.updateParams(PARAMS) host.trigger.add("MessageReceived", self.MessageReceivedTrigger) host.trigger.add("sendMessage", self.MessageSendTrigger) def addLanguage(self, mess_data): message = mess_data['message'] if len(message) == 1 and message.keys()[0] == '': msg = message.values()[0] lang = identifier.classify(msg)[0] mess_data["message"] = {lang: msg} return mess_data def MessageReceivedTrigger(self, client, message_elt, post_treat): """ Check if source is linked and repeat message, else do nothing """ lang_detect = self.host.memory.getParamA(NAME, CATEGORY, profile_key=client.profile) if lang_detect: post_treat.addCallback(self.addLanguage) return True def MessageSendTrigger(self, client, data, pre_xml_treatments, post_xml_treatments): lang_detect = self.host.memory.getParamA(NAME, CATEGORY, profile_key=client.profile) if lang_detect: self.addLanguage(data) return True sat-0.6.1.1+hg20180208/src/plugins/plugin_misc_text_syntaxes.py0000755000175500017600000003102713243470025024136 0ustar debaclelocal_src#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing various text syntaxes # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.internet import defer from twisted.internet.threads import deferToThread from sat.core import exceptions try: from lxml import html from lxml.html import clean except ImportError: raise exceptions.MissingModule(u"Missing module lxml, please download/install it from http://lxml.de/") from cgi import escape import re CATEGORY = D_("Composition") NAME = "Syntax" _SYNTAX_XHTML = "XHTML" _SYNTAX_CURRENT = "@CURRENT@" # TODO: check/adapt following list # list initialy based on feedparser list (http://pythonhosted.org/feedparser/html-sanitization.html) STYLES_WHITELIST = ("azimuth", "background-color", "border-bottom-color", "border-collapse", "border-color", "border-left-color", "border-right-color", "border-top-color", "clear", "color", "cursor", "direction", "display", "elevation", "float", "font", "font-family", "font-size", "font-style", "font-variant", "font-weight", "height", "letter-spacing", "line-height", "overflow", "pause", "pause-after", "pause-before", "pitch", "pitch-range", "richness", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "text-align", "text-decoration", "text-indent", "unicode-bidi", "vertical-align", "voice-family", "volume", "white-space", "width") SAFE_ATTRS = html.defs.safe_attrs.union(('style', 'poster', 'controls')) STYLES_VALUES_REGEX = r'^(' + '|'.join(['([a-z-]+)', # alphabetical names '(#[0-9a-f]+)', # hex value '(\d+(.\d+)? *(|%|em|ex|px|in|cm|mm|pt|pc))', # values with units (or not) 'rgb\( *((\d+(.\d+)?), *){2}(\d+(.\d+)?) *\)', # rgb function 'rgba\( *((\d+(.\d+)?), *){3}(\d+(.\d+)?) *\)', # rgba function ]) + ') *(!important)?$' # we accept "!important" at the end STYLES_ACCEPTED_VALUE = re.compile(STYLES_VALUES_REGEX) PLUGIN_INFO = { C.PI_NAME: "Text syntaxes", C.PI_IMPORT_NAME: "TEXT-SYNTAXES", C.PI_TYPE: "MISC", C.PI_PROTOCOLS: [], C.PI_DEPENDENCIES: [], C.PI_MAIN: "TextSyntaxes", C.PI_HANDLER: "no", C.PI_DESCRIPTION: _("""Management of various text syntaxes (XHTML-IM, Markdown, etc)""") } class TextSyntaxes(object): """ Text conversion class XHTML utf-8 is used as intermediate language for conversions """ OPT_DEFAULT = "DEFAULT" OPT_HIDDEN = "HIDDEN" OPT_NO_THREAD = "NO_THREAD" SYNTAX_XHTML = _SYNTAX_XHTML SYNTAX_MARKDOWN = "markdown" SYNTAX_TEXT = "text" syntaxes = {} default_syntax = SYNTAX_XHTML params = """ %(options)s """ params_data = { 'category_name': CATEGORY, 'category_label': _(CATEGORY), 'name': NAME, 'label': _(NAME), 'syntaxes': syntaxes, } def __init__(self, host): log.info(_("Text syntaxes plugin initialization")) self.host = host self.addSyntax(self.SYNTAX_XHTML, lambda xhtml: defer.succeed(xhtml), lambda xhtml: defer.succeed(xhtml), TextSyntaxes.OPT_NO_THREAD) # TODO: text => XHTML should add to url like in frontends # it's probably best to move sat_frontends.tools.strings to sat.tools.common or similar self.addSyntax(self.SYNTAX_TEXT, lambda text: escape(text), lambda xhtml: self._removeMarkups(xhtml), [TextSyntaxes.OPT_HIDDEN]) try: import markdown, html2text def _html2text(html, baseurl=''): h = html2text.HTML2Text(baseurl=baseurl) h.body_width = 0 # do not truncate the lines, it breaks the long URLs return h.handle(html) self.addSyntax(self.SYNTAX_MARKDOWN, markdown.markdown, _html2text, [TextSyntaxes.OPT_DEFAULT]) except ImportError: log.warning(u"markdown or html2text not found, can't use Markdown syntax") log.info(u"You can download/install them from https://pythonhosted.org/Markdown/ and https://github.com/Alir3z4/html2text/") host.bridge.addMethod("syntaxConvert", ".plugin", in_sign='sssbs', out_sign='s', async=True, method=self.convert) host.bridge.addMethod("syntaxGet", ".plugin", in_sign='s', out_sign='s', method=self.getSyntax) def _updateParamOptions(self): data_synt = TextSyntaxes.syntaxes default_synt = TextSyntaxes.default_syntax syntaxes = [] for syntax in data_synt.keys(): flags = data_synt[syntax]["flags"] if TextSyntaxes.OPT_HIDDEN not in flags: syntaxes.append(syntax) syntaxes.sort(key=lambda synt: synt.lower()) options = [] for syntax in syntaxes: selected = 'selected="true"' if syntax == default_synt else '' options.append(u'