summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/doap.xml596
-rw-r--r--doc/source/commands.rst10
-rw-r--r--doc/source/dev/e2ee.rst52
-rw-r--r--doc/source/dev/index.rst1
-rw-r--r--doc/source/dev/slix.rst4
-rw-r--r--doc/source/keys.rst4
-rw-r--r--plugins/b64.py53
-rw-r--r--plugins/bob.py2
-rw-r--r--plugins/dice.py4
-rw-r--r--plugins/display_corrections.py2
-rw-r--r--plugins/emoji_ascii.py58
-rw-r--r--plugins/figlet.py22
-rw-r--r--plugins/irc.py2
-rw-r--r--plugins/lastlog.py64
-rw-r--r--plugins/link.py2
-rw-r--r--plugins/marquee.py4
-rw-r--r--plugins/mirror.py2
-rw-r--r--plugins/mpd_client.py2
-rw-r--r--plugins/otr.py18
-rw-r--r--plugins/ping.py12
-rw-r--r--plugins/quote.py2
-rw-r--r--plugins/reorder.py2
-rw-r--r--plugins/replace.py4
-rw-r--r--plugins/send_delayed.py2
-rw-r--r--plugins/server_part.py4
-rw-r--r--plugins/stoi.py2
-rw-r--r--plugins/tell.py2
-rw-r--r--plugins/time_marker.py6
-rw-r--r--plugins/vcard.py12
-rw-r--r--poezio/common.py8
-rw-r--r--poezio/config.py7
-rw-r--r--poezio/core/commands.py137
-rw-r--r--poezio/core/completions.py9
-rw-r--r--poezio/core/core.py27
-rw-r--r--poezio/core/handlers.py8
-rw-r--r--poezio/events.py89
-rw-r--r--poezio/multiuserchat.py3
-rw-r--r--poezio/plugin.py4
-rw-r--r--poezio/plugin_e2ee.py291
-rw-r--r--poezio/plugin_manager.py4
-rw-r--r--poezio/tabs/basetabs.py193
-rw-r--r--poezio/tabs/conversationtab.py54
-rw-r--r--poezio/tabs/muclisttab.py1
-rw-r--r--poezio/tabs/muctab.py122
-rw-r--r--poezio/tabs/privatetab.py49
-rw-r--r--poezio/tabs/rostertab.py73
-rw-r--r--poezio/windows/image.py2
-rw-r--r--poezio/windows/text_win.py4
48 files changed, 1699 insertions, 336 deletions
diff --git a/data/doap.xml b/data/doap.xml
new file mode 100644
index 00000000..77f70afe
--- /dev/null
+++ b/data/doap.xml
@@ -0,0 +1,596 @@
+<?xml version="1.0"?>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
+ <name>poezio</name>
+
+ <created>2010-01-10</created>
+
+ <shortdesc xml:lang="en">Free console XMPP client</shortdesc>
+ <shortdesc xml:lang="fr">Client XMPP libre en console</shortdesc>
+
+ <description xml:lang="en">Free and modern console XMPP client written in Python with the ncurses library</description>
+ <description xml:lang="fr">Client console XMPP libre et moderne, écrit en Python avec la bibliothèque ncurses</description>
+
+ <homepage rdf:resource="https://poez.io/"/>
+ <!-- TODO: https://github.com/ewilderj/doap/issues/51 -->
+ <!--<doc rdf:resource="https://doc.poez.io/"/>-->
+ <download-page rdf:resource="https://dev.louiz.org/projects/poezio/files"/>
+ <bug-database rdf:resource="https://dev.louiz.org/projects/poezio/issues"/>
+ <!-- See https://github.com/ewilderj/doap/issues/53 -->
+ <developer-forum rdf:resource="xmpp:poezio@muc.poez.io?join"/>
+ <support-forum rdf:resource="xmpp:poezio@muc.poez.io?join"/>
+
+ <license rdf:resource="https://git.poez.io/poezio/plain/COPYING"/>
+
+ <!-- See https://github.com/ewilderj/doap/issues/49 -->
+ <language>en</language>
+
+ <!-- TODO: https://github.com/ewilderj/doap/issues/40 -->
+ <!--<logo rdf:resource="https://poez.io/img/logo.png"/>-->
+ <!-- TODO: https://github.com/ewilderj/doap/issues/50 -->
+ <!--<screenshot rdf:resource="https://poez.io/img/screenshot.png"/>-->
+
+ <programming-language>Python</programming-language>
+
+ <os>Linux</os>
+ <os>macOS</os>
+ <os>FreeBSD</os>
+ <os>OpenBSD</os>
+ <os>NetBSD</os>
+
+ <!-- TODO: Categories are URIs, find a better location for them. -->
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-console"/>
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-ncurses"/>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Link Mauve</foaf:name>
+ <foaf:homepage rdf:resource="https://linkmauve.fr/"/>
+ <foaf:mbox_sha1sum>aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f</foaf:mbox_sha1sum>
+ </foaf:Person>
+ </maintainer>
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>louiz’</foaf:name>
+ <foaf:homepage rdf:resource="https://louiz.org/"/>
+ <foaf:mbox_sha1sum>a867767905969a4915147374e3a064f97cdf5d61</foaf:mbox_sha1sum>
+ </foaf:Person>
+ </maintainer>
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>mathieui</foaf:name>
+ <foaf:homepage rdf:resource="https://mathieui.net/"/>
+ <foaf:mbox_sha1sum>c14292b375a7cec3f39872aa8524c66a1d9106cf</foaf:mbox_sha1sum>
+ </foaf:Person>
+ </maintainer>
+
+ <repository>
+ <GitRepository>
+ <browse rdf:resource="https://git.poez.io/poezio/"/>
+ <location rdf:resource="https://git.poez.io/poezio.git"/>
+ </GitRepository>
+ </repository>
+
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/>
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/>
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html"/>
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html"/>
+ <!-- TODO: Report a bug to support that in poezio. -->
+ <!--<implements rdf:resource="https://xmpp.org/rfcs/rfc5122.html"/>-->
+
+ <xmpp:software>
+ <xmpp:Client>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.9</xmpp:version>
+ <xmpp:since>0.7.2</xmpp:since>
+ <xmpp:note>used for ad-hoc commands</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0012.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0027.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.4</xmpp:version>
+ <xmpp:since>0.7.5</xmpp:since>
+ <xmpp:until>6cc1360a3a999c4384531e4f6576144040886768</xmpp:until>
+ <xmpp:note>plugin</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.4</xmpp:version>
+ <xmpp:since>0.5</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.27.1</xmpp:version>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.7.5</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0049.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.7.5</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0050.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.2.2</xmpp:version>
+ <xmpp:since>0.9</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0054.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ <xmpp:note>viewing only</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.13.5</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ <xmpp:note>only the PEP subset</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0070.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0.1</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0071.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.5.1</xmpp:version>
+ <xmpp:since>0.7.2</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>2.4</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ <xmpp:note>only for password change</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.1</xmpp:version>
+ <xmpp:since>0.7.2</xmpp:since>
+ <xmpp:note>also displayed in group chat</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0091.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.4</xmpp:version>
+ <xmpp:since>0.7.2</xmpp:since>
+ <xmpp:note>deprecated, will be removed in a future release</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0092.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.7</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0107.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0108.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.3</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.5.1</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0118.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0163.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0172.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0175.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.5</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0178.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.9</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.9</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.3</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ <xmpp:note>missing a view to manage blocks</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0196.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.5.2</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0202.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.7</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.7.2</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0224.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.7.5</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0231.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ <xmpp:note>plugin, sending-only</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0245.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.6</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0249.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.9</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0257.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.9</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.11.0</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0296.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.2</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ <xmpp:note>only used for Carbons</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.8</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0334.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.2</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.2</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0364.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.7.5</xmpp:since>
+ <xmpp:note>plugin</xmpp:note>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0378.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.0.1</xmpp:version>
+ <xmpp:since>0.10</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ <xmpp:supports>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>0.1</xmpp:version>
+ <xmpp:since>0.11</xmpp:since>
+ </xmpp:SupportedXep>
+ </xmpp:supports>
+ </xmpp:Client>
+ </xmpp:software>
+
+ <release>
+ <Version>
+ <revision>0.12.1</revision>
+ <created>2018-09-12</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/poezio/-/archive/v0.12.1/poezio-v0.12.1.tar.gz"/>
+ </Version>
+ </release>
+ <!-- TODO: https://github.com/ewilderj/doap/issues/52 -->
+ <release>
+ <Version>
+ <revision>0.12</revision>
+ <created>2018-08-13</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/poezio/-/archive/v0.12/poezio-v0.12.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.11</revision>
+ <created>2017-01-31</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/118/poezio-0.11.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.10</revision>
+ <created>2016-10-09</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/102/poezio-0.10.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.9</revision>
+ <created>2015-07-31</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/91/poezio-0.9.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.8.1</revision>
+ <created>2014-03-20</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/52/poezio-0.8.1.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.8</revision>
+ <created>2014-02-22</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/43/poezio-0.8.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.5.2</revision>
+ <created>2012-??-??</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/19/poezio-0.7.5.2.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.5.1</revision>
+ <created>2012-??-??</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/18/poezio-0.7.5.1.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.5</revision>
+ <created>2012-05-25</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/20/poezio-0.7.5.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.2</revision>
+ <created>2011-11-08</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/21/poezio-0.7.2.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.1</revision>
+ <created>2011-02-02</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/22/poezio-0.7.1.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7</revision>
+ <created>2011-01-14</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/23/poezio-0.7.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.6.2</revision>
+ <created>2010-07-21</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/24/poezio-0.6.2.tar.xz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.6.1</revision>
+ <created>2010-06-13</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/26/poezio-0.6.1.tar.bz2"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.6</revision>
+ <created>2010-06-13</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/27/poezio-0.6.tar.bz2"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.5.1</revision>
+ <created>2010-02-02</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/28/poezio-0.5.1.tar.bz2"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.5</revision>
+ <created>2010-02-01</created>
+ <file-release rdf:resource="https://dev.louiz.org/attachments/29/poezio-0.5.tar.bz2"/>
+ </Version>
+ </release>
+</Project>
+</rdf:RDF>
diff --git a/doc/source/commands.rst b/doc/source/commands.rst
index 3f3bb475..bea44fe0 100644
--- a/doc/source/commands.rst
+++ b/doc/source/commands.rst
@@ -218,12 +218,12 @@ These commands work in *any* tab.
/invitations
Show the pending invitations.
- /impromptu
+ /impromptu
**Usage:** ``/impromptu <jid> [jid ..]``
Invite specified JIDs into a newly created room.
- .. versionadded:: 0.13
+ .. versionadded:: 0.13
/activity
**Usage:** ``/activity [<general> [specific] [comment]]``
@@ -316,6 +316,12 @@ These commands will work in any conversation tab (MultiUserChat, Private, or
/clear
Clear the current buffer.
+ /scrollback
+ /sb
+ **Usage:** ``/scrollback end home clear status goto <+|-linecount>|<linenum>|<timestamp>``
+
+ Allows to go to the given line or message in the window.
+
.. _muctab-commands:
MultiUserChat tab commands
diff --git a/doc/source/dev/e2ee.rst b/doc/source/dev/e2ee.rst
new file mode 100644
index 00000000..23304512
--- /dev/null
+++ b/doc/source/dev/e2ee.rst
@@ -0,0 +1,52 @@
+End-to-end Encryption API documentation
+=======================================
+
+E2EEPlugin
+----------
+
+.. module:: poezio.plugin_e2ee
+
+
+.. autoclass:: E2EEPlugin
+ :members: decrypt, encrypt, encryption_name, encryption_short_name, eme_ns, replace_body_with_eme, stanza_encryption, tag_whitelist
+
+
+Please refer to :py:class:`~BasePlugin` for more information on how to
+write plugins.
+
+Example plugins
+---------------
+
+**Example 1:** Base64 plugin
+
+.. code-block:: python
+
+ from base64 import b64decode, b64encode
+ from poezio.plugin_e2ee import E2EEPlugin
+ from slixmpp import Message
+
+
+ class Plugin(E2EEPlugin):
+ """Base64 Plugin"""
+
+ encryption_name = 'base64'
+ encryption_short_name = 'b64'
+ eme_ns = 'urn:xmpps:base64:0'
+
+ # This encryption mechanism is using <body/> as a container
+ replace_body_with_eme = False
+
+ def decrypt(self, message: Message, _tab) -> None:
+ """
+ Decrypt base64
+ """
+ body = message['body']
+ message['body'] = b64decode(body.encode()).decode()
+
+ def encrypt(self, message: Message, _tab) -> None:
+ """
+ Encrypt to base64
+ """
+ # TODO: Stop using <body/> for this. Put the encoded payload in another element.
+ body = message['body']
+ message['body'] = b64encode(body.encode()).decode()
diff --git a/doc/source/dev/index.rst b/doc/source/dev/index.rst
index 21ea6253..630abfad 100644
--- a/doc/source/dev/index.rst
+++ b/doc/source/dev/index.rst
@@ -14,6 +14,7 @@ About plugins
:maxdepth: 2
plugin
+ e2ee
events
slix
xep
diff --git a/doc/source/dev/slix.rst b/doc/source/dev/slix.rst
index 3c06e349..50f9dd07 100644
--- a/doc/source/dev/slix.rst
+++ b/doc/source/dev/slix.rst
@@ -1,5 +1,5 @@
-SleekXMPP classes
-=================
+Slixmpp classes
+===============
.. module:: slixmpp
diff --git a/doc/source/keys.rst b/doc/source/keys.rst
index ae641c26..dc5fa35b 100644
--- a/doc/source/keys.rst
+++ b/doc/source/keys.rst
@@ -1,7 +1,7 @@
.. _keys-page:
-Keys
-====
+Keyboard Shortcuts
+==================
This file describes the default keys of poezio and explains how to
configure them.
diff --git a/plugins/b64.py b/plugins/b64.py
new file mode 100644
index 00000000..d56ac5b3
--- /dev/null
+++ b/plugins/b64.py
@@ -0,0 +1,53 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# Copyright © 2019 Maxime “pep” Buquet <pep@bouah.net>
+#
+# Distributed under terms of the zlib license.
+
+"""
+Usage
+-----
+
+Base64 encryption plugin.
+
+This plugin also respects security guidelines listed in XEP-0419.
+
+.. glossary::
+ /b64
+ **Usage:** ``/b64``
+
+ This command enables encryption of outgoing messages for the current
+ tab.
+"""
+
+from base64 import b64decode, b64encode
+from poezio.plugin_e2ee import E2EEPlugin
+from slixmpp import Message
+
+
+class Plugin(E2EEPlugin):
+ """Base64 Plugin"""
+
+ encryption_name = 'base64'
+ encryption_short_name = 'b64'
+ eme_ns = 'urn:xmpps:base64:0'
+
+ # This encryption mechanism is using <body/> as a container
+ replace_body_with_eme = False
+
+ def decrypt(self, message: Message, _tab) -> None:
+ """
+ Decrypt base64
+ """
+ body = message['body']
+ message['body'] = b64decode(body.encode()).decode()
+
+ def encrypt(self, message: Message, _tab) -> None:
+ """
+ Encrypt to base64
+ """
+ # TODO: Stop using <body/> for this. Put the encoded payload in another element.
+ body = message['body']
+ message['body'] = b64encode(body.encode()).decode()
diff --git a/plugins/bob.py b/plugins/bob.py
index be56ef4a..2d733e25 100644
--- a/plugins/bob.py
+++ b/plugins/bob.py
@@ -37,7 +37,7 @@ class Plugin(BasePlugin):
default_config = {'bob': {'max_size': 2048, 'max_age': 86400}}
def init(self):
- for tab in tabs.ConversationTab, tabs.PrivateTab, tabs.MucTab:
+ for tab in tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab, tabs.MucTab:
self.api.add_tab_command(
tab,
'bob',
diff --git a/plugins/dice.py b/plugins/dice.py
index 376ed26a..f92604e3 100644
--- a/plugins/dice.py
+++ b/plugins/dice.py
@@ -64,7 +64,7 @@ class Plugin(BasePlugin):
default_config = {"dice": {"refresh": 0.5, "default_duration": 5}}
def init(self):
- for tab_t in [tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab]:
+ for tab_t in [tabs.MucTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab]:
self.api.add_tab_command(
tab_t,
'roll',
@@ -95,7 +95,7 @@ class Plugin(BasePlugin):
is_muctab = isinstance(tab, tabs.MucTab)
msg_id = tab.last_sent_message["id"]
increment = self.config.get('refresh')
- roll = DiceRoll(duration, num_dice, is_muctab, tab.name, msg_id,
+ roll = DiceRoll(duration, num_dice, is_muctab, tab.jid, msg_id,
increment)
event = self.api.create_delayed_event(increment, self.delayed_event,
roll)
diff --git a/plugins/display_corrections.py b/plugins/display_corrections.py
index 22eb196d..e9e8a2e4 100644
--- a/plugins/display_corrections.py
+++ b/plugins/display_corrections.py
@@ -29,7 +29,7 @@ from poezio import tabs
class Plugin(BasePlugin):
def init(self):
- for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
+ for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'display_corrections',
diff --git a/plugins/emoji_ascii.py b/plugins/emoji_ascii.py
new file mode 100644
index 00000000..6629c50e
--- /dev/null
+++ b/plugins/emoji_ascii.py
@@ -0,0 +1,58 @@
+# poezio emoji_ascii plugin
+#
+# Will translate received Emoji to :emoji: for better display on text terminals,
+# and outgoing :emoji: into Emoji on the wire.
+#
+# Requires emojis.json.gz (MIT licensed) from:
+#
+# git clone https://github.com/vdurmont/emoji-java
+# gzip -9 < ./src/main/resources/emojis.json > poezio/plugins/emojis.json.gz
+
+# TODOs:
+# 1. it messes up your log files (doesn't log original message, logs mutilated :emoji: instead)
+# 2. Doesn't work on outgoing direct messages
+# 3. Doesn't detect pastes, corrupts jabber:x:foobar
+# 4. no auto-completion of emoji aliases
+# 5. coloring of converted Emojis to be able to differentiate them from incoming ASCII
+
+import gzip
+import json
+import os
+import re
+
+from poezio.plugin import BasePlugin
+
+class Plugin(BasePlugin):
+ emoji_to_ascii = {}
+ ascii_to_emoji = {}
+ emoji_pattern = None
+ alias_pattern = None
+
+ def init(self):
+ emoji_map_file_name = os.path.abspath(os.path.dirname(__file__) + '/emojis.json.gz')
+ emoji_map_data = gzip.open(emoji_map_file_name, 'r').read().decode('utf-8')
+ emoji_map = json.loads(emoji_map_data)
+ for e in emoji_map:
+ self.emoji_to_ascii[e['emoji']] = ':%s:' % e['aliases'][0]
+ for alias in e['aliases']:
+ # work around :iq: and similar country code misdetection
+ flag = re.match('^[a-z][a-z]$', alias) and "flag" in e["tags"]
+ if not flag:
+ self.ascii_to_emoji[':%s:' % alias] = e['emoji']
+ self.emoji_pattern = re.compile('|'.join(self.emoji_to_ascii.keys()).replace('*', '\*'))
+ self.alias_pattern = re.compile('|'.join(self.ascii_to_emoji.keys()).replace('+', '\+'))
+
+ self.api.add_event_handler('muc_msg', self.emoji2alias)
+ self.api.add_event_handler('conversation_msg', self.emoji2alias)
+ self.api.add_event_handler('private_msg', self.emoji2alias)
+
+ self.api.add_event_handler('muc_say', self.alias2emoji)
+ self.api.add_event_handler('private_say', self.alias2emoji)
+ self.api.add_event_handler('conversation_say', self.alias2emoji)
+
+
+ def emoji2alias(self, msg, tab):
+ msg['body'] = self.emoji_pattern.sub(lambda m: self.emoji_to_ascii[m.group()], msg['body'])
+
+ def alias2emoji(self, msg, tab):
+ msg['body'] = self.alias_pattern.sub(lambda m: self.ascii_to_emoji[m.group()], msg['body'])
diff --git a/plugins/figlet.py b/plugins/figlet.py
index b8fcb813..4d4c7577 100644
--- a/plugins/figlet.py
+++ b/plugins/figlet.py
@@ -11,15 +11,35 @@ Say something in a Chat tab.
.. note:: Can create fun things when used with :ref:`The rainbow plugin <rainbow-plugin>`.
"""
-from poezio.plugin import BasePlugin
+
import subprocess
+from poezio.plugin import BasePlugin
+
+
+def is_figlet() -> bool:
+ """Ensure figlet exists"""
+ process = subprocess.Popen(
+ ['which', 'figlet'],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ return process.wait() == 0
class Plugin(BasePlugin):
def init(self):
+ if not is_figlet():
+ self.api.information(
+ 'Couldn\'t find the figlet program. '
+ 'Please install it and reload the plugin.',
+ 'Error',
+ )
+ return None
+
self.api.add_event_handler('muc_say', self.figletize)
self.api.add_event_handler('conversation_say', self.figletize)
self.api.add_event_handler('private_say', self.figletize)
+ return None
def figletize(self, msg, tab):
process = subprocess.Popen(
diff --git a/plugins/irc.py b/plugins/irc.py
index eeef128c..9d981c91 100644
--- a/plugins/irc.py
+++ b/plugins/irc.py
@@ -376,7 +376,7 @@ class Plugin(BasePlugin):
"""
gateway = self.config.get('gateway', 'irc.poez.io')
current = self.api.current_tab()
- current_jid = common.safeJID(current.name)
+ current_jid = current.jid
if not current_jid.server == gateway:
self.api.information(
'The current tab does not appear to be an IRC one', 'Warning')
diff --git a/plugins/lastlog.py b/plugins/lastlog.py
new file mode 100644
index 00000000..314ca75e
--- /dev/null
+++ b/plugins/lastlog.py
@@ -0,0 +1,64 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# Copyright © 2018 Maxime “pep” Buquet
+# Copyright © 2019 Madhur Garg
+#
+# Distributed under terms of the zlib license. See the COPYING file.
+
+"""
+ Search provided string in the buffer and return all results on the screen
+"""
+
+import re
+from poezio.plugin import BasePlugin
+from poezio import tabs
+from poezio.text_buffer import Message, TextBuffer
+
+
+def add_line(text_buffer: TextBuffer, text: str) -> None:
+ """Adds a textual entry in the TextBuffer"""
+ text_buffer.add_message(
+ text,
+ None, # Time
+ None, # Nickname
+ None, # Nick Color
+ False, # History
+ None, # User
+ False, # Highlight
+ None, # Identifier
+ None, # str_time
+ None, # Jid
+ )
+
+
+class Plugin(BasePlugin):
+ """Lastlog Plugin"""
+
+ def init(self):
+ for tab in tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab, tabs.MucTab:
+ self.api.add_tab_command(
+ tab,
+ 'lastlog',
+ self.command_lastlog,
+ usage='<keyword>',
+ help='Search <keyword> in the buffer and returns results'
+ 'on the screen')
+
+ def command_lastlog(self, input_):
+ """Define lastlog command"""
+
+ text_buffer = self.api.current_tab()._text_buffer
+ search_re = re.compile(input_, re.I)
+
+ res = []
+ add_line(text_buffer, "Lastlog:")
+ for message in text_buffer.messages:
+ if message.nickname is not None and \
+ search_re.search(message.txt) is not None:
+ res.append(message)
+ add_line(text_buffer, "%s" % (message.txt))
+ add_line(text_buffer, "End of Lastlog")
+ self.api.current_tab().text_win.pos = 0
+ self.api.current_tab().core.refresh_window()
diff --git a/plugins/link.py b/plugins/link.py
index 352d403d..59a60c78 100644
--- a/plugins/link.py
+++ b/plugins/link.py
@@ -97,7 +97,7 @@ app_mapping = {
class Plugin(BasePlugin):
def init(self):
- for _class in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
+ for _class in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'link',
diff --git a/plugins/marquee.py b/plugins/marquee.py
index bad06301..80bfbfeb 100644
--- a/plugins/marquee.py
+++ b/plugins/marquee.py
@@ -56,7 +56,7 @@ class Plugin(BasePlugin):
}
def init(self):
- for tab_t in [tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab]:
+ for tab_t in [tabs.MucTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab]:
self.add_tab_command(
tab_t, 'marquee', self.command_marquee,
'Replicate the <marquee/> behavior in a message')
@@ -68,7 +68,7 @@ class Plugin(BasePlugin):
tab.command_say(args)
is_muctab = isinstance(tab, tabs.MucTab)
msg_id = tab.last_sent_message["id"]
- jid = tab.name
+ jid = tab.jid
event = self.api.create_delayed_event(
self.config.get("refresh"), self.delayed_event, jid, args, msg_id,
diff --git a/plugins/mirror.py b/plugins/mirror.py
index 116d16b1..55c429a3 100644
--- a/plugins/mirror.py
+++ b/plugins/mirror.py
@@ -16,7 +16,7 @@ from poezio import tabs
class Plugin(BasePlugin):
def init(self):
- for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
+ for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'mirror',
diff --git a/plugins/mpd_client.py b/plugins/mpd_client.py
index a8893999..f1eea902 100644
--- a/plugins/mpd_client.py
+++ b/plugins/mpd_client.py
@@ -57,7 +57,7 @@ import mpd
class Plugin(BasePlugin):
def init(self):
- for _class in (tabs.ConversationTab, tabs.MucTab, tabs.PrivateTab):
+ for _class in (tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.MucTab, tabs.PrivateTab):
self.api.add_tab_command(
_class,
'mpd',
diff --git a/plugins/otr.py b/plugins/otr.py
index 909a4ea5..2ddc332b 100644
--- a/plugins/otr.py
+++ b/plugins/otr.py
@@ -325,7 +325,7 @@ def hl(tab):
if tab.state != 'current':
tab.state = 'private'
- conv_jid = safeJID(tab.name)
+ conv_jid = tab.jid
if 'private' in config.get('beep_on', 'highlight private').split():
if not config.get_by_tabname(
'disable_beep', conv_jid.bare, default=False):
@@ -806,7 +806,7 @@ class Plugin(BasePlugin):
On message sent
"""
name = tab.name
- jid = safeJID(tab.name)
+ jid = tab.jid
format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
@@ -846,7 +846,7 @@ class Plugin(BasePlugin):
elif not is_relevant(tab) and ctx and (
ctx.state == STATE_ENCRYPTED
or ctx.getPolicy('REQUIRE_ENCRYPTION')):
- contact = roster[tab.name]
+ contact = roster[tab.jid.bare]
res = []
if contact:
res = [resource.jid for resource in contact.resources]
@@ -884,13 +884,13 @@ class Plugin(BasePlugin):
return self.core.command.help('otr')
action = args.pop(0)
tab = self.api.current_tab()
- name = tab.name
+ name = tab.jid.full
format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
'normal': '\x19%s}' % dump_tuple(get_theme().COLOR_NORMAL_TEXT),
- 'jid': name,
- 'bare_jid': safeJID(name).bare
+ 'jid': tab.jid.full,
+ 'bare_jid': tab.jid.bare,
}
if action == 'end': # close the session
@@ -991,12 +991,12 @@ class Plugin(BasePlugin):
question = secret = None
tab = self.api.current_tab()
- name = tab.name
+ name = tab.jid.full
format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- 'jid': name,
- 'bare_jid': safeJID(name).bare
+ 'jid': tab.jid.full,
+ 'bare_jid': tab.jid.bare,
}
ctx = self.get_context(name)
diff --git a/plugins/ping.py b/plugins/ping.py
index 4868ccf9..7e0098aa 100644
--- a/plugins/ping.py
+++ b/plugins/ping.py
@@ -22,6 +22,7 @@ Command
the current interlocutor.
"""
+from slixmpp import InvalidJID, JID
from poezio.decorators import command_args_parser
from poezio.plugin import BasePlugin
from poezio.roster import roster
@@ -57,7 +58,7 @@ class Plugin(BasePlugin):
help='Send an XMPP ping to jid (see XEP-0199).',
short='Send a ping.',
completion=self.completion_ping)
- for _class in (tabs.PrivateTab, tabs.ConversationTab):
+ for _class in (tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'ping',
@@ -116,7 +117,7 @@ class Plugin(BasePlugin):
def command_private_ping(self, arg):
if arg:
return self.command_ping(arg)
- self.command_ping(self.api.current_tab().name)
+ self.command_ping(self.api.current_tab().jid)
@command_args_parser.raw
def command_muc_ping(self, arg):
@@ -124,10 +125,13 @@ class Plugin(BasePlugin):
return
user = self.api.current_tab().get_user_by_name(arg)
if user:
- jid = safeJID(self.api.current_tab().name)
+ jid = self.api.current_tab().jid
jid.resource = user.nick
else:
- jid = safeJID(arg)
+ try:
+ jid = JID(arg)
+ except InvalidJID:
+ return self.api.information('Invalid JID: %s' % arg, 'Error')
self.command_ping(jid.full)
@command_args_parser.raw
diff --git a/plugins/quote.py b/plugins/quote.py
index b412cd9a..20bd9133 100644
--- a/plugins/quote.py
+++ b/plugins/quote.py
@@ -56,7 +56,7 @@ log = logging.getLogger(__name__)
class Plugin(BasePlugin):
def init(self):
- for _class in (tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab):
+ for _class in (tabs.MucTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab):
self.api.add_tab_command(
_class,
'quote',
diff --git a/plugins/reorder.py b/plugins/reorder.py
index 7308196d..32fa6639 100644
--- a/plugins/reorder.py
+++ b/plugins/reorder.py
@@ -112,7 +112,7 @@ def parse_runtime_tablist(tablist):
i += 1
result = check_tab(tab)
if result:
- props.append((i, '%s:%s' % (result, tab.name)))
+ props.append((i, '%s:%s' % (result, tab.jid.full)))
return props
diff --git a/plugins/replace.py b/plugins/replace.py
index 7e259dab..9646a817 100644
--- a/plugins/replace.py
+++ b/plugins/replace.py
@@ -102,11 +102,11 @@ def replace_random_user(message, tab):
if isinstance(tab, tabs.MucTab):
return random.choice(tab.users).nick
elif isinstance(tab, tabs.PrivateTab):
- return random.choice([JID(tab.name).resource, tab.own_nick])
+ return random.choice([tab.jid.resource, tab.own_nick])
else:
# that doesn’t make any sense. By why use this pattern in a
# ConversationTab anyway?
- return str(tab.name)
+ return tab.jid.full
def replace_dice(message, tab):
diff --git a/plugins/send_delayed.py b/plugins/send_delayed.py
index 846fccd1..e8b00027 100644
--- a/plugins/send_delayed.py
+++ b/plugins/send_delayed.py
@@ -28,7 +28,7 @@ from poezio import timed_events
class Plugin(BasePlugin):
def init(self):
- for _class in (tabs.PrivateTab, tabs.ConversationTab, tabs.MucTab):
+ for _class in (tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.MucTab):
self.api.add_tab_command(
_class,
'send_delayed',
diff --git a/plugins/server_part.py b/plugins/server_part.py
index 7a71d94b..f29b4099 100644
--- a/plugins/server_part.py
+++ b/plugins/server_part.py
@@ -39,7 +39,7 @@ class Plugin(BasePlugin):
if not args and not isinstance(current_tab, MucTab):
return self.core.command_help('server_part')
elif not args:
- jid = safeJID(current_tab.name).bare
+ jid = current_tab.jid.bare
message = None
elif len(args) == 1:
jid = safeJID(args[0]).domain
@@ -60,6 +60,6 @@ class Plugin(BasePlugin):
serv_list = set()
for tab in self.core.get_tabs(MucTab):
if tab.joined:
- serv = safeJID(tab.name).server
+ serv = tab.jid.server
serv_list.add(serv)
return Completion(the_input.new_completion, sorted(serv_list), 1, ' ')
diff --git a/plugins/stoi.py b/plugins/stoi.py
index 04d84881..78c4ed70 100644
--- a/plugins/stoi.py
+++ b/plugins/stoi.py
@@ -28,7 +28,7 @@ char_we_dont_want = string.punctuation + ' ’„“”…«»'
class Plugin(BasePlugin):
def init(self):
- for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.ConversationTab):
+ for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'stoi',
diff --git a/plugins/tell.py b/plugins/tell.py
index 43a91d8b..614c1ef5 100644
--- a/plugins/tell.py
+++ b/plugins/tell.py
@@ -75,7 +75,7 @@ class Plugin(BasePlugin):
if not self.tabs.get(tab):
self.api.information('No message queued.', 'Info')
return
- build = ['Messages queued for %s:' % tab.name]
+ build = ['Messages queued for %s:' % tab.jid.bare]
for nick, messages in self.tabs[tab].items():
build.append(' for %s:' % nick)
for message in messages:
diff --git a/plugins/time_marker.py b/plugins/time_marker.py
index bd6af1c4..76f7e589 100644
--- a/plugins/time_marker.py
+++ b/plugins/time_marker.py
@@ -36,7 +36,7 @@ from datetime import datetime, timedelta
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler("muc_msg", self.on_muc_msg)
- # Dict of MucTab.name: last_message date, so we don’t have to
+ # Dict of MucTab.jid.bare: last_message date, so we don’t have to
# retrieve the messages of the given muc to look for the last
# message’s date each time. Also, now that I think about it, the
# date of the message is not event kept in the Message object, so…
@@ -66,8 +66,8 @@ class Plugin(BasePlugin):
res += "%s seconds, " % seconds
return res[:-2]
- last_message_date = self.last_messages.get(tab.name)
- self.last_messages[tab.name] = datetime.now()
+ last_message_date = self.last_messages.get(tab.jid.bare)
+ self.last_messages[tab.jid.bare] = datetime.now()
if last_message_date:
delta = datetime.now() - last_message_date
if delta >= timedelta(0, self.config.get('delay', 900)):
diff --git a/plugins/vcard.py b/plugins/vcard.py
index 643dd569..e3a776e3 100644
--- a/plugins/vcard.py
+++ b/plugins/vcard.py
@@ -61,7 +61,7 @@ class Plugin(BasePlugin):
help='Send an XMPP vcard request to jid (see XEP-0054).',
short='Send a vcard request.',
completion=self.completion_vcard)
- for _class in (tabs.PrivateTab, tabs.ConversationTab):
+ for _class in (tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'vcard',
@@ -273,7 +273,7 @@ class Plugin(BasePlugin):
if arg:
self.command_vcard(arg)
return
- self.command_vcard(self.api.current_tab().name)
+ self.command_vcard(self.api.current_tab().jid.full)
@command_args_parser.raw
def command_muc_vcard(self, arg):
@@ -282,10 +282,12 @@ class Plugin(BasePlugin):
return
user = self.api.current_tab().get_user_by_name(arg)
if user:
- # No need to use safeJID here, we already know the JID is valid.
- jid = JID(self.api.current_tab().name + '/' + user.nick)
+ jid = self.api.current_tab().jid.bare + '/' + user.nick
else:
- jid = safeJID(arg)
+ try:
+ jid = safeJID(arg)
+ except InvalidJID:
+ return self.api.information('Invalid JID: %s' % arg, 'Error')
self._get_vcard(jid)
@command_args_parser.raw
diff --git a/poezio/common.py b/poezio/common.py
index a39f8145..ba179310 100644
--- a/poezio/common.py
+++ b/poezio/common.py
@@ -16,10 +16,13 @@ import os
import subprocess
import time
import string
+import logging
from slixmpp import JID, InvalidJID, Message
from poezio.poezio_shlex import shlex
+log = logging.getLogger(__name__)
+
def _get_output_of_command(command: str) -> Optional[List[str]]:
"""
@@ -459,4 +462,9 @@ def safeJID(*args, **kwargs) -> JID:
try:
return JID(*args, **kwargs)
except InvalidJID:
+ log.debug(
+ 'safeJID caught an invalidJID exception: %r, %r',
+ args, kwargs,
+ exc_info=True,
+ )
return JID('')
diff --git a/poezio/config.py b/poezio/config.py
index 89b75d94..acd5f293 100644
--- a/poezio/config.py
+++ b/poezio/config.py
@@ -20,6 +20,7 @@ from configparser import RawConfigParser, NoOptionError, NoSectionError
from pathlib import Path
from shutil import copy2
from typing import Callable, Dict, List, Optional, Union, Tuple
+from slixmpp import JID
from poezio.args import parse_args
from poezio import xdg
@@ -214,7 +215,7 @@ class Config(RawConfigParser):
def get_by_tabname(self,
option,
- tabname,
+ tabname: str,
fallback=True,
fallback_server=True,
default=''):
@@ -224,6 +225,8 @@ class Config(RawConfigParser):
in the section, we search for the global option if fallback is
True. And we return `default` as a fallback as a last resort.
"""
+ if isinstance(tabname, JID):
+ tabname = tabname.full
if self.default and (not default) and fallback:
default = self.default.get(DEFSECTION, {}).get(option, '')
if tabname in self.sections():
@@ -447,7 +450,7 @@ class Config(RawConfigParser):
RawConfigParser.set(self, section, option, value)
if not self.write_in_file(section, option, value):
return ('Unable to write in the config file', 'Error')
- if 'password' in option and 'eval_password' not in option:
+ if isinstance(option, str) and 'password' in option and 'eval_password' not in option:
value = '********'
return ("%s=%s" % (option, value), 'Info')
diff --git a/poezio/core/commands.py b/poezio/core/commands.py
index 4dbb5611..05e7421b 100644
--- a/poezio/core/commands.py
+++ b/poezio/core/commands.py
@@ -8,6 +8,7 @@ log = logging.getLogger(__name__)
import asyncio
from xml.etree import cElementTree as ET
+from typing import List, Optional, Tuple
from slixmpp import JID, InvalidJID
from slixmpp.exceptions import XMPPError
@@ -23,6 +24,8 @@ from poezio.bookmarks import Bookmark
from poezio.common import safeJID
from poezio.config import config, DEFAULT_CONFIG, options as config_opts
from poezio import multiuserchat as muc
+from poezio.contact import Contact
+from poezio import windows
from poezio.plugin import PluginConfig
from poezio.roster import roster
from poezio.theming import dump_tuple, get_theme
@@ -133,7 +136,7 @@ class CommandCore:
current.send_chat_state('inactive')
for tab in self.core.tabs:
if isinstance(tab, tabs.MucTab) and tab.joined:
- muc.change_show(self.core.xmpp, tab.name, tab.own_nick, show,
+ muc.change_show(self.core.xmpp, tab.jid, tab.own_nick, show,
msg)
if hasattr(tab, 'directed_presence'):
del tab.directed_presence
@@ -151,7 +154,7 @@ class CommandCore:
jid, ptype, status = args[0], args[1], args[2]
if jid == '.' and isinstance(self.core.tabs.current_tab, tabs.ChatTab):
- jid = self.core.tabs.current_tab.name
+ jid = self.core.tabs.current_tab.jid
if ptype == 'available':
ptype = None
try:
@@ -258,7 +261,7 @@ class CommandCore:
self.core.refresh_window()
@command_args_parser.quoted(0, 1)
- def list(self, args):
+ def list(self, args: List[str]) -> None:
"""
/list [server]
Opens a MucListTab containing the list of the room in the specified server
@@ -266,12 +269,18 @@ class CommandCore:
if args is None:
return self.help('list')
elif args:
- jid = safeJID(args[0])
+ try:
+ jid = JID(args[0])
+ except InvalidJID:
+ return self.core.information('Invalid server %r' % jid, 'Error')
else:
if not isinstance(self.core.tabs.current_tab, tabs.MucTab):
return self.core.information('Please provide a server',
'Error')
- jid = safeJID(self.core.tabs.current_tab.name)
+ jid = self.core.tabs.current_tab.jid
+ if jid is None or not jid.domain:
+ return None
+ jid = JID(jid.domain)
list_tab = tabs.MucListTab(self.core, jid)
self.core.add_tab(list_tab, True)
cb = list_tab.on_muc_list_item_received
@@ -297,20 +306,23 @@ class CommandCore:
tab = self.core.tabs.current_tab
if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
return (None, None)
- room = safeJID(tab.name).bare
+ room = tab.jid.bare
nick = tab.own_nick
return (room, nick)
- def _parse_join_jid(self, jid_string):
+ def _parse_join_jid(self, jid_string: str) -> Tuple[Optional[str], Optional[str]]:
# we try to join a server directly
- if jid_string.startswith('@'):
- server_root = True
- info = safeJID(jid_string[1:])
- else:
- info = safeJID(jid_string)
- server_root = False
+ try:
+ if jid_string.startswith('@'):
+ server_root = True
+ info = JID(jid_string[1:])
+ else:
+ info = JID(jid_string)
+ server_root = False
+ except InvalidJID:
+ return (None, None)
- set_nick = ''
+ set_nick = '' # type: Optional[str]
if len(jid_string) > 1 and jid_string.startswith('/'):
set_nick = jid_string[1:]
elif info.resource:
@@ -322,7 +334,7 @@ class CommandCore:
if not isinstance(tab, tabs.MucTab):
room, set_nick = (None, None)
else:
- room = tab.name
+ room = tab.jid.bare
if not set_nick:
set_nick = tab.own_nick
else:
@@ -332,10 +344,8 @@ class CommandCore:
# check if the current room's name has a server
if room.find('@') == -1 and not server_root:
tab = self.core.tabs.current_tab
- if isinstance(tab, tabs.MucTab):
- if tab.name.find('@') != -1:
- domain = safeJID(tab.name).domain
- room += '@%s' % domain
+ if isinstance(tab, tabs.MucTab) and tab.domain:
+ room += '@%s' % tab.domain
return (room, set_nick)
@command_args_parser.quoted(0, 2)
@@ -425,7 +435,7 @@ class CommandCore:
nick = None
if not jid:
tab = self.core.tabs.current_tab
- roomname = tab.name
+ roomname = tab.jid.bare
if tab.joined and tab.own_nick != self.core.own_nick:
nick = tab.own_nick
if password is None and tab.password is not None:
@@ -439,7 +449,7 @@ class CommandCore:
tab = self.core.tabs.current_tab
if not isinstance(tab, tabs.MucTab):
return
- roomname = tab.name
+ roomname = tab.jid.bare
bookmark = self.core.bookmarks[roomname]
if bookmark is None:
bookmark = Bookmark(roomname)
@@ -458,9 +468,9 @@ class CommandCore:
def _add_wildcard_bookmarks(self, method):
new_bookmarks = []
for tab in self.core.get_tabs(tabs.MucTab):
- bookmark = self.core.bookmarks[tab.name]
+ bookmark = self.core.bookmarks[tab.jid.bare]
if not bookmark:
- bookmark = Bookmark(tab.name, autojoin=True, method=method)
+ bookmark = Bookmark(tab.jid.bare, autojoin=True, method=method)
new_bookmarks.append(bookmark)
else:
bookmark.method = method
@@ -497,8 +507,8 @@ class CommandCore:
if not args:
tab = self.core.tabs.current_tab
- if isinstance(tab, tabs.MucTab) and self.core.bookmarks[tab.name]:
- self.core.bookmarks.remove(tab.name)
+ if isinstance(tab, tabs.MucTab) and self.core.bookmarks[tab.jid.bare]:
+ self.core.bookmarks.remove(tab.jid.bare)
self.core.bookmarks.save(self.core.xmpp, callback=cb)
else:
self.core.information('No bookmark to remove', 'Info')
@@ -509,6 +519,71 @@ class CommandCore:
else:
self.core.information('No bookmark to remove', 'Info')
+ @command_args_parser.quoted(0, 1)
+ def command_accept(self, args):
+ """
+ Accept a JID. Authorize it AND subscribe to it
+ """
+ if not args:
+ tab = self.core.tabs.current_tab
+ RosterInfoTab = tabs.RosterInfoTab
+ if not isinstance(tab, RosterInfoTab):
+ return self.core.information('No JID specified', 'Error')
+ else:
+ item = tab.selected_row
+ if isinstance(item, Contact):
+ jid = item.bare_jid
+ else:
+ return self.core.information('No subscription to accept', 'Warning')
+ else:
+ jid = safeJID(args[0]).bare
+ nodepart = safeJID(jid).user
+ jid = safeJID(jid)
+ # crappy transports putting resources inside the node part
+ if '\\2f' in nodepart:
+ jid.user = nodepart.split('\\2f')[0]
+ contact = roster[jid]
+ if contact is None:
+ return self.core.information('No subscription to accept', 'Warning')
+ contact.pending_in = False
+ roster.modified()
+ self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
+ self.core.xmpp.client_roster.send_last_presence()
+ if contact.subscription in ('from',
+ 'none') and not contact.pending_out:
+ self.core.xmpp.send_presence(
+ pto=jid, ptype='subscribe', pnick=self.core.own_nick)
+ self.core.information('%s is now authorized' % jid, 'Roster')
+
+ @command_args_parser.quoted(1)
+ def command_add(self, args):
+ """
+ Add the specified JID to the roster, and automatically
+ accept the reverse subscription
+ """
+ if args is None:
+ tab = self.core.tabs.current_tab
+ ConversationTab = tabs.ConversationTab
+ if isinstance(tab, ConversationTab):
+ jid = tab.general_jid
+ if jid in roster and roster[jid].subscription in ('to', 'both'):
+ return self.core.information('Already subscribed.', 'Roster')
+ roster.add(jid)
+ roster.modified()
+ return self.core.information('%s was added to the roster' % jid, 'Roster')
+ else:
+ return self.core.information('No JID specified', 'Error')
+ jid = safeJID(safeJID(args[0]).bare)
+ if not str(jid):
+ self.core.information(
+ 'The provided JID (%s) is not valid' % (args[0], ), 'Error')
+ return
+ if jid in roster and roster[jid].subscription in ('to', 'both'):
+ return self.core.information('Already subscribed.', 'Roster')
+ roster.add(jid)
+ roster.modified()
+ self.core.information('%s was added to the roster' % jid, 'Roster')
+
@command_args_parser.ignored
def command_reconnect(self):
"""
@@ -536,7 +611,8 @@ class CommandCore:
theme.COLOR_INFORMATION_TEXT),
})
for option_name, option_value in section.items():
- if 'password' in option_name and 'eval_password' not in option_name:
+ if isinstance(option_name, str) and \
+ 'password' in option_name and 'eval_password' not in option_name:
option_value = '********'
lines.append(
'%s\x19%s}=\x19o%s' %
@@ -546,7 +622,8 @@ class CommandCore:
elif len(args) == 1:
option = args[0]
value = config.get(option)
- if 'password' in option and 'eval_password' not in option and value is not None:
+ if isinstance(option, str) and \
+ 'password' in option and 'eval_password' not in option and value is not None:
value = '********'
if value is None and '=' in option:
args = option.split('=', 1)
@@ -595,7 +672,7 @@ class CommandCore:
info = plugin_config.set_and_save(option, value, section)
else:
if args[0] == '.':
- name = safeJID(self.core.tabs.current_tab.name).bare
+ name = self.core.tabs.current_tab.jid.bare
if not name:
self.core.information(
'Invalid tab to use the "." argument.', 'Error')
@@ -663,11 +740,11 @@ class CommandCore:
message = args[1]
else:
if isinstance(tab, tabs.MucTab):
- domain = safeJID(tab.name).domain
+ domain = tab.jid.domain
else:
return self.core.information("No server specified", "Error")
for tab in self.core.get_tabs(tabs.MucTab):
- if JID(tab.name).domain == domain:
+ if tab.jid.domain == domain:
tab.leave_room(message)
tab.join()
diff --git a/poezio/core/completions.py b/poezio/core/completions.py
index 87bb2d47..84de77ca 100644
--- a/poezio/core/completions.py
+++ b/poezio/core/completions.py
@@ -161,8 +161,8 @@ class CompletionCore:
muc_serv_list = []
for tab in self.core.get_tabs(
tabs.MucTab): # TODO, also from an history
- if tab.name not in muc_serv_list:
- muc_serv_list.append(safeJID(tab.name).server)
+ if tab.jid.server not in muc_serv_list:
+ muc_serv_list.append(tab.jid.server)
if muc_serv_list:
return Completion(
the_input.new_completion, muc_serv_list, 1, quotify=False)
@@ -284,7 +284,7 @@ class CompletionCore:
rooms = []
for tab in self.core.get_tabs(tabs.MucTab):
if tab.joined:
- rooms.append(tab.name)
+ rooms.append(tab.jid.bare)
rooms.sort()
return Completion(
the_input.new_completion, rooms, n, '', quotify=True)
@@ -346,8 +346,7 @@ class CompletionCore:
"""Completion for /server_cycle"""
serv_list = set()
for tab in self.core.get_tabs(tabs.MucTab):
- serv = safeJID(tab.name).server
- serv_list.add(serv)
+ serv_list.add(tab.jid.server)
return Completion(the_input.new_completion, sorted(serv_list), 1, ' ')
def set(self, the_input):
diff --git a/poezio/core/core.py b/poezio/core/core.py
index 6f840dc2..26543add 100644
--- a/poezio/core/core.py
+++ b/poezio/core/core.py
@@ -19,7 +19,7 @@ from typing import Callable, Dict, List, Optional, Set, Tuple, Type
from xml.etree import cElementTree as ET
from functools import partial
-from slixmpp import JID
+from slixmpp import JID, InvalidJID
from slixmpp.util import FileSystemPerJidCache
from slixmpp.xmlstream.handler import Callback
from slixmpp.exceptions import IqError, IqTimeout
@@ -936,7 +936,15 @@ class Core:
nick = self.own_nick
localpart = uuid.uuid4().hex
- room = '{!s}@{!s}'.format(localpart, default_muc)
+ room_str = '{!s}@{!s}'.format(localpart, default_muc)
+ try:
+ room = JID(room_str)
+ except InvalidJID:
+ self.information(
+ 'The generated XMPP address is invalid: {!s}'.format(room_str),
+ 'Error'
+ )
+ return None
self.open_new_room(room, nick).join()
iq = self._impromptu_room_form(room)
@@ -1758,6 +1766,21 @@ class Core:
shortdesc="Bookmark a room online.",
completion=self.completion.bookmark)
self.register_command(
+ 'accept',
+ self.command.command_accept,
+ usage='[jid]',
+ desc='Allow the provided JID (or the selected contact '
+ 'in your roster), to see your presence.',
+ shortdesc='Allow a user your presence.',)
+ self.register_command(
+ 'add',
+ self.command.command_add,
+ usage='<jid>',
+ desc='Add the specified JID to your roster, ask them to'
+ ' allow you to see his presence, and allow them to'
+ ' see your presence.',
+ shortdesc='Add a user to your roster.')
+ self.register_command(
'reconnect',
self.command.command_reconnect,
usage="[reconnect]",
diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py
index 614cabe4..c17f3761 100644
--- a/poezio/core/handlers.py
+++ b/poezio/core/handlers.py
@@ -235,7 +235,7 @@ class HandlerCore:
# Differentiate both type of messages, and call the appropriate handler.
jid_from = message['from']
for tab in self.core.get_tabs(tabs.MucTab):
- if tab.name == jid_from.bare:
+ if tab.jid.bare == jid_from.bare:
if jid_from.resource:
self.on_groupchat_private_message(message, sent=False)
return
@@ -247,7 +247,7 @@ class HandlerCore:
"""
jid_from = message['from']
for tab in self.core.get_tabs(tabs.MucTab):
- if tab.name == jid_from.bare:
+ if tab.jid.bare == jid_from.bare:
if jid_from.full == jid_from.bare:
self.core.room_error(message, jid_from.bare)
else:
@@ -1408,12 +1408,12 @@ class HandlerCore:
jid_from = message['from']
self.core.information('%s requests your attention!' % jid_from, 'Info')
for tab in self.core.tabs:
- if tab.name == jid_from:
+ if tab.jid == jid_from:
tab.state = 'attention'
self.core.refresh_tab_win()
return
for tab in self.core.tabs:
- if tab.name == jid_from.bare:
+ if tab.jid.bare == jid_from.bare:
tab.state = 'attention'
self.core.refresh_tab_win()
return
diff --git a/poezio/events.py b/poezio/events.py
index 3bfe5156..b34eef32 100644
--- a/poezio/events.py
+++ b/poezio/events.py
@@ -9,6 +9,7 @@ The list of available events is here:
http://poezio.eu/doc/en/plugins.html#_poezio_events
"""
+from collections import OrderedDict
from typing import Callable, Dict, List
@@ -21,49 +22,56 @@ class EventHandler:
"""
def __init__(self):
- self.events = {
- 'highlight': [],
- 'muc_say': [],
- 'muc_say_after': [],
- 'conversation_say': [],
- 'conversation_say_after': [],
- 'private_say': [],
- 'private_say_after': [],
- 'conversation_msg': [],
- 'private_msg': [],
- 'muc_msg': [],
- 'conversation_chatstate': [],
- 'muc_chatstate': [],
- 'private_chatstate': [],
- 'normal_presence': [],
- 'muc_presence': [],
- 'muc_join': [],
- 'joining_muc': [],
- 'changing_nick': [],
- 'muc_kick': [],
- 'muc_nickchange': [],
- 'muc_ban': [],
- 'send_normal_presence': [],
- 'ignored_private': [],
- 'tab_change': [],
- } # type: Dict[str, List[Callable]]
+ events = [
+ 'highlight',
+ 'muc_say',
+ 'muc_say_after',
+ 'conversation_say',
+ 'conversation_say_after',
+ 'private_say',
+ 'private_say_after',
+ 'conversation_msg',
+ 'private_msg',
+ 'muc_msg',
+ 'conversation_chatstate',
+ 'muc_chatstate',
+ 'private_chatstate',
+ 'normal_presence',
+ 'muc_presence',
+ 'muc_join',
+ 'joining_muc',
+ 'changing_nick',
+ 'muc_kick',
+ 'muc_nickchange',
+ 'muc_ban',
+ 'send_normal_presence',
+ 'ignored_private',
+ 'tab_change',
+ ]
+ self.events = {} # type: Dict[str, OrderedDict[int, List[Callable]]]
+ for event in events:
+ self.events[event] = OrderedDict()
def add_event_handler(self, name: str, callback: Callable,
- position=0) -> bool:
+ priority: int = 50) -> bool:
"""
Add a callback to a given event.
Note that if that event name doesn’t exist, it just returns False.
If it was successfully added, it returns True
- position: 0 means insert at the beginning, -1 means end
+ priority is a integer between 0 and 100. 0 is the highest priority and
+ will be called first. 100 is the lowest.
"""
+
if name not in self.events:
return False
callbacks = self.events[name]
- if position >= 0:
- callbacks.insert(position, callback)
- else:
- callbacks.append(callback)
+
+ # Clamp priority
+ priority = max(0, min(priority, 100))
+
+ entry = callbacks.setdefault(priority, [])
+ entry.append(callback)
return True
@@ -74,8 +82,9 @@ class EventHandler:
callbacks = self.events.get(name, None)
if callbacks is None:
return
- for callback in callbacks:
- callback(*args, **kwargs)
+ for priority in callbacks.values():
+ for callback in priority:
+ callback(*args, **kwargs)
def del_event_handler(self, name: str, callback: Callable):
"""
@@ -83,9 +92,13 @@ class EventHandler:
"""
if not name:
for callbacks in self.events.values():
- while callback in callbacks:
- callbacks.remove(callback)
+ for priority in callbacks.values():
+ for entry in priority[:]:
+ if entry == callback:
+ priority.remove(callback)
else:
callbacks = self.events[name]
- if callback in callbacks:
- callbacks.remove(callback)
+ for priority in callbacks.entries():
+ for entry in priority[:]:
+ if entry == callback:
+ priority.remove(callback)
diff --git a/poezio/multiuserchat.py b/poezio/multiuserchat.py
index 73a802b2..a58883c6 100644
--- a/poezio/multiuserchat.py
+++ b/poezio/multiuserchat.py
@@ -13,6 +13,7 @@ slix plugin
from xml.etree import cElementTree as ET
from poezio.common import safeJID
+from slixmpp import JID
from slixmpp.exceptions import IqError, IqTimeout
import logging
log = logging.getLogger(__name__)
@@ -67,7 +68,7 @@ def send_groupchat_message(xmpp, jid, line):
xmpp.send_message(mto=jid, mbody=line, mtype='groupchat')
-def change_show(xmpp, jid, own_nick, show, status):
+def change_show(xmpp, jid: JID, own_nick: str, show, status):
"""
Change our 'Show'
"""
diff --git a/poezio/plugin.py b/poezio/plugin.py
index 7e67d09c..0275e2f9 100644
--- a/poezio/plugin.py
+++ b/poezio/plugin.py
@@ -501,12 +501,12 @@ class BasePlugin(object, metaclass=SafetyMetaclass):
"""
return self.api.del_tab_command(tab_type, name)
- def add_event_handler(self, event_name, handler, position=0):
+ def add_event_handler(self, event_name, handler, *args, **kwargs):
"""
Add an event handler to the event event_name.
An optional position in the event handler list can be provided.
"""
- return self.api.add_event_handler(event_name, handler, position)
+ return self.api.add_event_handler(event_name, handler, *args, **kwargs)
def del_event_handler(self, event_name, handler):
"""
diff --git a/poezio/plugin_e2ee.py b/poezio/plugin_e2ee.py
new file mode 100644
index 00000000..b6c6d940
--- /dev/null
+++ b/poezio/plugin_e2ee.py
@@ -0,0 +1,291 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8 et ts=4 sts=4 sw=4
+#
+# Copyright © 2019 Maxime “pep” Buquet <pep@bouah.net>
+#
+# Distributed under terms of the zlib license. See COPYING file.
+
+"""
+ Interface for E2EE (End-to-end Encryption) plugins.
+"""
+
+from typing import Callable, Dict, List, Optional, Union
+
+from slixmpp import InvalidJID, JID, Message
+from slixmpp.xmlstream import StanzaBase
+from poezio.tabs import ConversationTab, DynamicConversationTab, PrivateTab, MucTab
+from poezio.plugin import BasePlugin
+
+import logging
+log = logging.getLogger(__name__)
+
+
+ChatTabs = Union[
+ MucTab,
+ DynamicConversationTab,
+ PrivateTab,
+]
+
+EME_NS = 'urn:xmpp:eme:0'
+EME_TAG = 'encryption'
+
+JCLIENT_NS = 'jabber:client'
+HINTS_NS = 'urn:xmpp:hints'
+
+
+class E2EEPlugin(BasePlugin):
+ """Interface for E2EE plugins.
+
+ This is a wrapper built on top of BasePlugin. It provides a base for
+ End-to-end Encryption mechanisms in poezio.
+
+ Plugin developers are excepted to implement the `decrypt` and
+ `encrypt` function, provide an encryption name (and/or short name),
+ and an eme namespace.
+
+ Once loaded, the plugin will attempt to decrypt any message that
+ contains an EME message that matches the one set.
+
+ The plugin will also register a command (using the short name) to
+ enable encryption per tab. It is only possible to have one encryption
+ mechanism per tab, even if multiple e2ee plugins are loaded.
+
+ The encryption status will be displayed in the status bar, using the
+ plugin short name, alongside the JID, nickname etc.
+ """
+
+ #: Specifies that the encryption mechanism does more than encrypting
+ #: `<body/>`.
+ stanza_encryption = False
+
+ #: Whitelist applied to messages when `stanza_encryption` is `False`.
+ tag_whitelist = list(map(lambda x: '{%s}%s' % (x[0], x[1]), [
+ (JCLIENT_NS, 'body'),
+ (EME_NS, EME_TAG),
+ (HINTS_NS, 'store'),
+ (HINTS_NS, 'no-copy'),
+ (HINTS_NS, 'no-store'),
+ (HINTS_NS, 'no-permanent-store'),
+ # TODO: Add other encryption mechanisms tags here
+ ]))
+
+ #: Replaces body with `eme <https://xmpp.org/extensions/xep-0380.html>`_
+ #: if set. Should be suitable for most plugins except those using
+ #: `<body/>` directly as their encryption container, like OTR, or the
+ #: example base64 plugin in poezio.
+ replace_body_with_eme = True
+
+ #: Encryption name, used in command descriptions, and logs. At least one
+ #: of `encryption_name` and `encryption_short_name` must be set.
+ encryption_name = None # type: Optional[str]
+
+ #: Encryption short name, used as command name, and also to display
+ #: encryption status in a tab. At least one of `encryption_name` and
+ #: `encryption_short_name` must be set.
+ encryption_short_name = None # type: Optional[str]
+
+ #: Required. https://xmpp.org/extensions/xep-0380.html.
+ eme_ns = None # type: Optional[str]
+
+ #: Used to figure out what messages to attempt decryption for. Also used
+ #: in combination with `tag_whitelist` to avoid removing encrypted tags
+ #: before sending.
+ encrypted_tags = None # type: Optional[List[Tuple[str, str]]]
+
+ # Static map, to be able to limit to one encryption mechanism per tab at a
+ # time
+ _enabled_tabs = {} # type: Dict[JID, Callable]
+
+ def init(self):
+ if self.encryption_name is None and self.encryption_short_name is None:
+ raise NotImplementedError
+
+ if self.eme_ns is None:
+ raise NotImplementedError
+
+ if self.encryption_name is None:
+ self.encryption_name = self.encryption_short_name
+ if self.encryption_short_name is None:
+ self.encryption_short_name = self.encryption_name
+
+ # Ensure decryption is done before everything, so that other handlers
+ # don't have to know about the encryption mechanism.
+ self.api.add_event_handler('muc_msg', self._decrypt, priority=0)
+ self.api.add_event_handler('conversation_msg', self._decrypt, priority=0)
+ self.api.add_event_handler('private_msg', self._decrypt, priority=0)
+
+ # Ensure encryption is done after everything, so that whatever can be
+ # encrypted is encrypted, and no plain element slips in.
+ # Using a stream filter might be a bit too much, but at least we're
+ # sure poezio is not sneaking anything past us.
+ self.core.xmpp.add_filter('out', self._encrypt)
+
+ for tab_t in (DynamicConversationTab, PrivateTab, MucTab):
+ self.api.add_tab_command(
+ tab_t,
+ self.encryption_short_name,
+ self._toggle_tab,
+ usage='',
+ short='Toggle {} encryption for tab.'.format(self.encryption_name),
+ help='Toggle automatic {} encryption for tab.'.format(self.encryption_name),
+ )
+
+ ConversationTab.add_information_element(
+ self.encryption_short_name,
+ self._display_encryption_status,
+ )
+ MucTab.add_information_element(
+ self.encryption_short_name,
+ self._display_encryption_status,
+ )
+ PrivateTab.add_information_element(
+ self.encryption_short_name,
+ self._display_encryption_status,
+ )
+
+ def cleanup(self):
+ ConversationTab.remove_information_element(self.encryption_short_name)
+ MucTab.remove_information_element(self.encryption_short_name)
+ PrivateTab.remove_information_element(self.encryption_short_name)
+
+ def _display_encryption_status(self, jid_s: str) -> str:
+ """
+ Return information to display in the infobar if encryption is
+ enabled for the JID.
+ """
+
+ try:
+ jid = JID(jid_s)
+ except InvalidJID:
+ return ""
+
+ if self._encryption_enabled(jid):
+ return " " + self.encryption_short_name
+ return ""
+
+ def _toggle_tab(self, _input: str) -> None:
+ jid = self.api.current_tab().jid # type: JID
+
+ if self._encryption_enabled(jid):
+ del self._enabled_tabs[jid]
+ self.api.information(
+ '{} encryption disabled for {}'.format(self.encryption_name, jid),
+ 'Info',
+ )
+ else:
+ self._enabled_tabs[jid] = self.encrypt
+ self.api.information(
+ '{} encryption enabled for {}'.format(self.encryption_name, jid),
+ 'Info',
+ )
+
+ def _encryption_enabled(self, jid: JID) -> bool:
+ return jid in self._enabled_tabs and self._enabled_tabs[jid] == self.encrypt
+
+ def _decrypt(self, message: Message, tab: ChatTabs) -> None:
+
+ has_eme = False
+ if message.xml.find('{%s}%s' % (EME_NS, EME_TAG)) is not None and \
+ message['eme']['namespace'] == self.eme_ns:
+ has_eme = True
+
+ has_encrypted_tag = False
+ if not has_eme and self.encrypted_tags is not None:
+ for (namespace, tag) in self.encrypted_tags:
+ if message.xml.find('{%s}%s' % (namespace, tag)) is not None:
+ has_encrypted_tag = True
+ break
+
+ if not has_eme and not has_encrypted_tag:
+ return None
+
+ log.debug('Received %s message: %r', self.encryption_name, message['body'])
+
+ self.decrypt(message, tab)
+
+ log.debug('Decrypted %s message: %r', self.encryption_name, message['body'])
+ return None
+
+ def _encrypt(self, stanza: StanzaBase) -> Optional[StanzaBase]:
+ if not isinstance(stanza, Message) or stanza['type'] not in ('chat', 'groupchat'):
+ return stanza
+ message = stanza
+
+ tab = self.api.current_tab()
+ jid = tab.jid
+ if not self._encryption_enabled(jid):
+ return message
+
+ log.debug('Sending %s message: %r', self.encryption_name, message)
+
+ has_body = message.xml.find('{%s}%s' % (JCLIENT_NS, 'body')) is not None
+
+ # Drop all messages that don't contain a body if the plugin doesn't do
+ # Stanza Encryption
+ if not self.stanza_encryption and not has_body:
+ log.debug(
+ '%s plugin: Dropping message as it contains no body, and is '
+ 'not doesn\'t do stanza encryption: %r',
+ self.encryption_name,
+ message,
+ )
+ return None
+
+ # Call the enabled encrypt method
+ self._enabled_tabs[jid](message, tab)
+
+ if has_body:
+ # Only add EME tag if the message has a body.
+ # Per discussion in jdev@:
+ # The receiving client needs to know the message contains
+ # meaningful information or not to display notifications to the
+ # user, and not display anything when it's e.g., a chatstate.
+ # This does leak the fact that the encrypted payload contains a
+ # message.
+ message['eme']['namespace'] = self.eme_ns
+ message['eme']['name'] = self.encryption_name
+
+ if self.replace_body_with_eme:
+ self.core.xmpp['xep_0380'].replace_body_with_eme(message)
+
+ # Filter stanza with the whitelist. Plugins doing stanza encryption
+ # will have to include these in their encrypted container beforehand.
+ whitelist = self.tag_whitelist
+ if self.encrypted_tags is not None:
+ whitelist += self.encrypted_tags
+
+ for elem in message.xml[:]:
+ if elem.tag not in whitelist:
+ message.xml.remove(elem)
+
+ log.debug('Encrypted %s message: %r', self.encryption_name, message)
+ return message
+
+ def decrypt(self, _message: Message, tab: ChatTabs):
+ """Decryption method
+
+ This is a method the plugin must implement. It is expected that this
+ method will edit the received message and return nothing.
+
+ :param message: Message to be decrypted.
+ :param tab: Tab the message is coming from.
+
+ :returns: None
+ """
+
+ raise NotImplementedError
+
+ def encrypt(self, _message: Message, tab: ChatTabs):
+ """Encryption method
+
+ This is a method the plugin must implement. It is expected that this
+ method will edit the received message and return nothing.
+
+ :param message: Message to be encrypted.
+ :param tab: Tab the message is going to.
+
+ :returns: None
+ """
+
+ raise NotImplementedError
diff --git a/poezio/plugin_manager.py b/poezio/plugin_manager.py
index 89849747..8275e6f9 100644
--- a/poezio/plugin_manager.py
+++ b/poezio/plugin_manager.py
@@ -253,7 +253,7 @@ class PluginManager:
if key in self.core.key_func:
del self.core.commands[key]
- def add_event_handler(self, module_name, event_name, handler, position=0):
+ def add_event_handler(self, module_name, event_name, handler, *args, **kwargs):
"""
Add an event handler. If event_name isn’t in the event list, assume
it is a slixmpp event.
@@ -261,7 +261,7 @@ class PluginManager:
eh = self.event_handlers[module_name]
eh.append((event_name, handler))
if event_name in self.core.events.events:
- self.core.events.add_event_handler(event_name, handler, position)
+ self.core.events.add_event_handler(event_name, handler, *args, **kwargs)
else:
self.core.xmpp.add_event_handler(event_name, handler)
diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py
index 213f39d7..e57f0064 100644
--- a/poezio/tabs/basetabs.py
+++ b/poezio/tabs/basetabs.py
@@ -18,20 +18,23 @@ import string
import time
from datetime import datetime
from xml.etree import cElementTree as ET
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional, Union
-from slixmpp import JID, Message
+from slixmpp import JID, InvalidJID, Message
from poezio.core.structs import Command, Completion, Status
from poezio import timed_events
from poezio import windows
from poezio import xhtml
+from poezio import poopt
+from math import ceil, log10
+from poezio.windows.funcs import truncate_nick, parse_attrs
from poezio.common import safeJID
from poezio.config import config
from poezio.decorators import refresh_wrapper
from poezio.logger import logger
from poezio.text_buffer import TextBuffer
-from poezio.theming import get_theme, dump_tuple
+from poezio.theming import to_curses_attr, get_theme, dump_tuple
from poezio.decorators import command_args_parser
log = logging.getLogger(__name__)
@@ -462,9 +465,15 @@ class ChatTab(Tab):
plugin_keys = {} # type: Dict[str, Callable]
message_type = 'chat'
- def __init__(self, core, jid=''):
+ def __init__(self, core, jid: Union[JID, str]):
Tab.__init__(self, core)
- self.name = jid
+
+ if not isinstance(jid, JID):
+ jid = JID(jid)
+ assert jid.domain
+ self._jid = jid
+
+ self._name = jid.full # type: Optional[str]
self.text_win = None
self.directed_presence = None
self._text_buffer = TextBuffer()
@@ -485,6 +494,12 @@ class ChatTab(Tab):
usage='<message>',
shortdesc='Send the message.')
self.register_command(
+ 'scrollback',
+ self.command_scrollback,
+ usage="end home clear status goto <+|-linecount>|<linenum>|<timestamp>",
+ shortdesc='Scrollback to the given line number, message, or clear the buffer.')
+ self.commands['sb'] = self.commands['scrollback']
+ self.register_command(
'xhtml',
self.command_xhtml,
usage='<custom xhtml>',
@@ -510,12 +525,42 @@ class ChatTab(Tab):
self._text_buffer.add_message(**message)
@property
+ def name(self) -> str:
+ if self._name is not None:
+ return self._name
+ return self._jid.full
+
+ @name.setter
+ def name(self, value: Union[JID, str]) -> None:
+ if isinstance(value, JID):
+ self.jid = value
+ elif isinstance(value, str):
+ try:
+ value = JID(value)
+ if value.domain:
+ self._jid = value
+ except InvalidJID:
+ self._name = value
+ else:
+ raise TypeError("Name %r must be of type JID or str." % value)
+
+ @property
+ def jid(self) -> JID:
+ return self._jid
+
+ @jid.setter
+ def jid(self, value: JID) -> None:
+ if not isinstance(value, JID):
+ raise TypeError("Jid %r must be of type JID." % value)
+ assert value.domain
+ self._jid = value
+
+ @property
def general_jid(self) -> JID:
raise NotImplementedError
def load_logs(self, log_nb: int) -> Optional[List[Dict[str, Any]]]:
- logs = logger.get_logs(safeJID(self.name).bare, log_nb)
- return logs
+ return logger.get_logs(self.jid.bare, log_nb)
def log_message(self,
txt: str,
@@ -525,7 +570,7 @@ class ChatTab(Tab):
"""
Log the messages in the archives.
"""
- name = safeJID(self.name).bare
+ name = self.jid.bare
if not logger.log_message(name, nickname, txt, date=time, typ=typ):
self.core.information('Unable to write in the log file', 'Error')
@@ -624,7 +669,7 @@ class ChatTab(Tab):
return msg
def get_dest_jid(self) -> JID:
- return self.name
+ return self.jid
@refresh_wrapper.always
def command_clear(self, ignored):
@@ -746,6 +791,132 @@ class ChatTab(Tab):
def command_say(self, line, correct=False):
pass
+ def goto_build_lines(self, new_date):
+ text_buffer = self._text_buffer
+ built_lines = []
+ message_count = 0
+ timestamp = config.get('show_timestamps')
+ nick_size = config.get('max_nick_length')
+ for message in text_buffer.messages:
+ # Build lines of a message
+ txt = message.txt
+ nick = truncate_nick(message.nickname, nick_size)
+ offset = 0
+ theme = get_theme()
+ if message.ack:
+ if message.ack > 0:
+ offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1
+ else:
+ offset += poopt.wcswidth(theme.CHAR_NACK) + 1
+ if nick:
+ offset += poopt.wcswidth(nick) + 2
+ if message.revisions > 0:
+ offset += ceil(log10(message.revisions + 1))
+ if message.me:
+ offset += 1
+ if timestamp:
+ if message.str_time:
+ offset += 1 + len(message.str_time)
+ if theme.CHAR_TIME_LEFT and message.str_time:
+ offset += 1
+ if theme.CHAR_TIME_RIGHT and message.str_time:
+ offset += 1
+ lines = poopt.cut_text(txt, self.text_win.width - offset - 1)
+ for line in lines:
+ built_lines.append(line)
+ # Find the message with timestamp less than or equal to the queried
+ # timestamp and goto that location in the tab.
+ if message.time <= new_date:
+ message_count += 1
+ if len(self.text_win.built_lines) - self.text_win.height >= len(built_lines):
+ self.text_win.pos = len(self.text_win.built_lines) - self.text_win.height - len(built_lines) + 1
+ else:
+ self.text_win.pos = 0
+ if message_count == 0:
+ self.text_win.scroll_up(len(self.text_win.built_lines))
+ self.core.refresh_window()
+
+ @command_args_parser.quoted(0, 2)
+ def command_scrollback(self, args):
+ """
+ /sb clear
+ /sb home
+ /sb end
+ /sb goto <+|-linecount>|<linenum>|<timestamp>
+ The format of timestamp must be ‘[dd[.mm]-<days ago>] hh:mi[:ss]’
+ """
+ if args is None or len(args) == 0:
+ args = ['end']
+ if len(args) == 1:
+ if args[0] == 'end':
+ self.text_win.scroll_down(len(self.text_win.built_lines))
+ self.core.refresh_window()
+ return
+ elif args[0] == 'home':
+ self.text_win.scroll_up(len(self.text_win.built_lines))
+ self.core.refresh_window()
+ return
+ elif args[0] == 'clear':
+ self._text_buffer.messages = []
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.core.refresh_window()
+ return
+ elif args[0] == 'status':
+ self.core.information('Total %s lines in this tab.' % len(self.text_win.built_lines), 'Info')
+ return
+ elif len(args) == 2 and args[0] == 'goto':
+ for fmt in ('%d %H:%M', '%d %H:%M:%S', '%d:%m %H:%M', '%d:%m %H:%M:%S', '%H:%M', '%H:%M:%S'):
+ try:
+ new_date = datetime.strptime(args[1], fmt)
+ if 'd' in fmt and 'm' in fmt:
+ new_date = new_date.replace(year=datetime.now().year)
+ elif 'd' in fmt:
+ new_date = new_date.replace(year=datetime.now().year, month=datetime.now().month)
+ else:
+ new_date = new_date.replace(year=datetime.now().year, month=datetime.now().month, day=datetime.now().day)
+ except ValueError:
+ pass
+ if args[1].startswith('-'):
+ # Check if the user is giving argument of type goto <-linecount> or goto [-<days ago>] hh:mi[:ss]
+ if ' ' in args[1]:
+ new_args = args[1].split(' ')
+ new_args[0] = new_args[0].strip('-')
+ new_date = datetime.now()
+ if new_args[0].isdigit():
+ new_date = new_date.replace(day=new_date.day - int(new_args[0]))
+ for fmt in ('%H:%M', '%H:%M:%S'):
+ try:
+ arg_date = datetime.strptime(new_args[1], fmt)
+ new_date = new_date.replace(hour=arg_date.hour, minute=arg_date.minute, second=arg_date.second)
+ except ValueError:
+ pass
+ else:
+ scroll_len = args[1].strip('-')
+ if scroll_len.isdigit():
+ self.text_win.scroll_down(int(scroll_len))
+ self.core.refresh_window()
+ return
+ elif args[1].startswith('+'):
+ scroll_len = args[1].strip('+')
+ if scroll_len.isdigit():
+ self.text_win.scroll_up(int(scroll_len))
+ self.core.refresh_window()
+ return
+ # Check for the argument of type goto <linenum>
+ elif args[1].isdigit():
+ if len(self.text_win.built_lines) - self.text_win.height >= int(args[1]):
+ self.text_win.pos = len(self.text_win.built_lines) - self.text_win.height - int(args[1])
+ self.core.refresh_window()
+ return
+ else:
+ self.text_win.pos = 0
+ self.core.refresh_window()
+ return
+ elif args[1] == '0':
+ args = ['home']
+ # new_date is the timestamp for which the user has queried.
+ self.goto_build_lines(new_date)
+
def on_line_up(self):
return self.text_win.scroll_up(1)
@@ -770,7 +941,7 @@ class ChatTab(Tab):
class OneToOneTab(ChatTab):
- def __init__(self, core, jid=''):
+ def __init__(self, core, jid):
ChatTab.__init__(self, core, jid)
self.__status = Status("", "")
@@ -801,7 +972,7 @@ class OneToOneTab(ChatTab):
return
self.__status = status
hide_status_change = config.get_by_tabname('hide_status_change',
- safeJID(self.name).bare)
+ self.jid.bare)
now = datetime.now()
dff = now - self.last_remote_message
if hide_status_change > -1 and dff.total_seconds() > hide_status_change:
diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py
index 60106527..f8490233 100644
--- a/poezio/tabs/conversationtab.py
+++ b/poezio/tabs/conversationtab.py
@@ -47,7 +47,6 @@ class ConversationTab(OneToOneTab):
self.nick = None
self.nick_sent = False
self.state = 'normal'
- self.name = jid # a conversation tab is linked to one specific full jid OR bare jid
self.text_win = windows.TextWin()
self._text_buffer.add_window(self.text_win)
self.upper_bar = windows.ConversationStatusMessageWin()
@@ -73,13 +72,6 @@ class ConversationTab(OneToOneTab):
shortdesc='Get the activity.',
completion=self.core.completion.last_activity)
self.register_command(
- 'add',
- self.command_add,
- desc='Add the current JID to your roster, ask them to'
- ' allow you to see his presence, and allow them to'
- ' see your presence.',
- shortdesc='Add a user to your roster.')
- self.register_command(
'invite',
self.core.command.impromptu,
desc='Invite people into an impromptu room.',
@@ -90,12 +82,13 @@ class ConversationTab(OneToOneTab):
@property
def general_jid(self):
- return safeJID(self.name).bare
+ return self.jid.bare
def get_info_header(self):
raise NotImplementedError
@staticmethod
+ @refresh_wrapper.always
def add_information_element(plugin_name, callback):
"""
Lets a plugin add its own information to the ConversationInfoWin
@@ -103,6 +96,7 @@ class ConversationTab(OneToOneTab):
ConversationTab.additional_information[plugin_name] = callback
@staticmethod
+ @refresh_wrapper.always
def remove_information_element(plugin_name):
del ConversationTab.additional_information[plugin_name]
@@ -130,7 +124,7 @@ class ConversationTab(OneToOneTab):
replaced = False
if correct or msg['replace']['id']:
msg['replace']['id'] = self.last_sent_message['id']
- if config.get_by_tabname('group_corrections', self.name):
+ if config.get_by_tabname('group_corrections', self.jid.full):
try:
self.modify_message(
msg['body'],
@@ -249,7 +243,7 @@ class ConversationTab(OneToOneTab):
"""
if args:
return self.core.command.version(args[0])
- jid = safeJID(self.name)
+ jid = self.jid
if not jid.resource:
if jid in roster:
resource = roster[jid].get_highest_priority_resource()
@@ -257,19 +251,6 @@ class ConversationTab(OneToOneTab):
self.core.xmpp.plugin['xep_0092'].get_version(
jid, callback=self.core.handler.on_version_result)
- @command_args_parser.ignored
- def command_add(self):
- """
- Add the current JID to the roster, and automatically
- accept the reverse subscription
- """
- jid = self.general_jid
- if jid in roster and roster[jid].subscription in ('to', 'both'):
- return self.core.information('Already subscribed.', 'Roster')
- roster.add(jid)
- roster.modified()
- self.core.information('%s was added to the roster' % jid, 'Roster')
-
def resize(self):
self.need_resize = False
if self.size.tab_degrade_y:
@@ -321,14 +302,13 @@ class ConversationTab(OneToOneTab):
self.input.refresh()
def get_nick(self):
- jid = safeJID(self.name)
- contact = roster[jid.bare]
+ contact = roster[self.jid.bare]
if contact:
- return contact.name or jid.user
+ return contact.name or self.jid.user
else:
if self.nick:
return self.nick
- return jid.user
+ return self.jid.user
def on_input(self, key, raw):
if not raw and key in self.key_func:
@@ -401,7 +381,7 @@ class ConversationTab(OneToOneTab):
def matching_names(self):
res = []
- jid = safeJID(self.name)
+ jid = self.jid
res.append((2, jid.bare))
res.append((1, jid.user))
contact = roster[self.name]
@@ -422,8 +402,8 @@ class DynamicConversationTab(ConversationTab):
def __init__(self, core, jid, resource=None):
self.locked_resource = None
- self.name = safeJID(jid).bare
ConversationTab.__init__(self, core, jid)
+ self.jid.resource = None
self.info_header = windows.DynamicConversationInfoWin()
self.register_command(
'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.')
@@ -447,7 +427,7 @@ class DynamicConversationTab(ConversationTab):
"""
Returns the bare jid.
"""
- return self.name
+ return self.jid.bare
def refresh(self):
"""
@@ -460,9 +440,9 @@ class DynamicConversationTab(ConversationTab):
self.text_win.refresh()
if display_bar:
- self.upper_bar.refresh(self.name, roster[self.name])
- displayed_jid = self.name
- self.get_info_header().refresh(displayed_jid, roster[self.name],
+ self.upper_bar.refresh(self.jid.bare, roster[self.jid.bare])
+ displayed_jid = self.jid.bare
+ self.get_info_header().refresh(displayed_jid, roster[self.jid.bare],
self.text_win, self.chatstate,
ConversationTab.additional_information)
if display_info_win:
@@ -475,8 +455,8 @@ class DynamicConversationTab(ConversationTab):
"""
Different from the parent class only for the info_header object.
"""
- displayed_jid = self.name
- self.get_info_header().refresh(displayed_jid, roster[self.name],
+ displayed_jid = self.jid.bare
+ self.get_info_header().refresh(displayed_jid, roster[self.jid.bare],
self.text_win, self.chatstate,
ConversationTab.additional_information)
self.input.refresh()
@@ -491,8 +471,8 @@ class StaticConversationTab(ConversationTab):
plugin_keys = {} # type: Dict[str, Callable]
def __init__(self, core, jid):
- assert (safeJID(jid).resource)
ConversationTab.__init__(self, core, jid)
+ assert jid.resource
self.info_header = windows.ConversationInfoWin()
self.resize()
self.update_commands()
diff --git a/poezio/tabs/muclisttab.py b/poezio/tabs/muclisttab.py
index aac25787..4c1e492f 100644
--- a/poezio/tabs/muclisttab.py
+++ b/poezio/tabs/muclisttab.py
@@ -60,6 +60,7 @@ class MucListTab(ListTab):
items = [(item[0].split('@')[0], item[0], item[2] or '', '')
for item in get_items()]
+ items = sorted(items, key=lambda item: item[0])
self.listview.set_lines(items)
self.info_header.message = 'Chatroom list on server %s' % self.name
if self.core.tabs.current_tab is self:
diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py
index 17abb369..81bb5f0b 100644
--- a/poezio/tabs/muctab.py
+++ b/poezio/tabs/muctab.py
@@ -14,10 +14,11 @@ import os
import random
import re
import functools
+from copy import copy
from datetime import datetime
from typing import Dict, Callable, List, Optional, Union, Set
-from slixmpp import JID
+from slixmpp import InvalidJID, JID
from poezio.tabs import ChatTab, Tab, SHOW_NAME
from poezio import common
@@ -63,7 +64,6 @@ class MucTab(ChatTab):
self.own_nick = nick
# self User object
self.own_user = None # type: Optional[User]
- self.name = jid
self.password = password
# buffered presences
self.presence_buffer = []
@@ -95,7 +95,7 @@ class MucTab(ChatTab):
@property
def general_jid(self):
- return self.name
+ return self.jid
def check_send_chat_state(self) -> bool:
"If we should send a chat state"
@@ -109,6 +109,7 @@ class MucTab(ChatTab):
return None
@staticmethod
+ @refresh_wrapper.always
def add_information_element(plugin_name: str, callback: Callable[[str], str]) -> None:
"""
Lets a plugin add its own information to the MucInfoWin
@@ -116,6 +117,7 @@ class MucTab(ChatTab):
MucTab.additional_information[plugin_name] = callback
@staticmethod
+ @refresh_wrapper.always
def remove_information_element(plugin_name: str) -> None:
"""
Lets a plugin add its own information to the MucInfoWin
@@ -126,14 +128,14 @@ class MucTab(ChatTab):
"""
The user do not want to send their config, send an iq cancel
"""
- muc.cancel_config(self.core.xmpp, self.name)
+ muc.cancel_config(self.core.xmpp, self.jid.bare)
self.core.close_tab()
def send_config(self, form):
"""
The user sends their config to the server
"""
- muc.configure_room(self.core.xmpp, self.name, form)
+ muc.configure_room(self.core.xmpp, self.jid.bare, form)
self.core.close_tab()
def join(self):
@@ -148,7 +150,7 @@ class MucTab(ChatTab):
seconds = None
muc.join_groupchat(
self.core,
- self.name,
+ self.jid.bare,
self.own_nick,
self.password,
status=status.message,
@@ -193,11 +195,11 @@ class MucTab(ChatTab):
self.add_message(msg, typ=2)
self.disconnect()
- muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick,
+ muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
message)
- self.core.disable_private_tabs(self.name, reason=msg)
+ self.core.disable_private_tabs(self.jid.bare, reason=msg)
else:
- muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick,
+ muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
message)
def change_affiliation(self,
@@ -225,7 +227,7 @@ class MucTab(ChatTab):
if nick_or_jid in [user.nick for user in self.users]:
muc.set_user_affiliation(
self.core.xmpp,
- self.name,
+ self.jid.bare,
affiliation,
nick=nick_or_jid,
callback=callback,
@@ -233,7 +235,7 @@ class MucTab(ChatTab):
else:
muc.set_user_affiliation(
self.core.xmpp,
- self.name,
+ self.jid.bare,
affiliation,
jid=safeJID(nick_or_jid),
callback=callback,
@@ -256,10 +258,14 @@ class MucTab(ChatTab):
return self.core.information(
'The role must be one of ' + ', '.join(valid_roles), 'Error')
- if not safeJID(self.name + '/' + nick):
+ try:
+ target_jid = copy(self.jid)
+ target_jid.resource = nick
+ except InvalidJID:
return self.core.information('Invalid nick', 'Info')
+
muc.set_user_role(
- self.core.xmpp, self.name, nick, reason, role, callback=callback)
+ self.core.xmpp, self.jid.bare, nick, reason, role, callback=callback)
@refresh_wrapper.conditional
def print_info(self, nick: str) -> bool:
@@ -295,7 +301,7 @@ class MucTab(ChatTab):
def change_topic(self, topic: str):
"""Change the current topic"""
- muc.change_subject(self.core.xmpp, self.name, topic)
+ muc.change_subject(self.core.xmpp, self.jid.bare, topic)
@refresh_wrapper.always
def show_topic(self):
@@ -324,7 +330,7 @@ class MucTab(ChatTab):
def recolor(self, random_colors=False):
"""Recolor the current MUC users"""
deterministic = config.get_by_tabname('deterministic_nick_colors',
- self.name)
+ self.jid.bare)
if deterministic:
for user in self.users:
if user is self.own_user:
@@ -373,7 +379,7 @@ class MucTab(ChatTab):
user.change_color(color)
config.set_and_save(nick, color, 'muc_colors')
nick_color_aliases = config.get_by_tabname('nick_color_aliases',
- self.name)
+ self.jid.bare)
if nick_color_aliases:
# if any user in the room has a nick which is an alias of the
# nick, update its color
@@ -400,12 +406,12 @@ class MucTab(ChatTab):
def get_nick(self) -> str:
if config.get('show_muc_jid'):
- return self.name
- bookmark = self.core.bookmarks[self.name]
+ return self.jid.bare
+ bookmark = self.core.bookmarks[self.jid.bare]
if bookmark is not None and bookmark.name:
return bookmark.name
# TODO: send the disco#info identity name here, if it exists.
- return safeJID(self.name).user
+ return self.jid.user
def get_text_window(self):
return self.text_win
@@ -446,7 +452,7 @@ class MucTab(ChatTab):
for status_code in presence.xml.findall(STATUS_XPATH):
status_codes.add(status_code.attrib['code'])
if presence['type'] == 'error':
- self.core.room_error(presence, self.name)
+ self.core.room_error(presence, self.jid.bare)
elif not self.joined:
own = '110' in status_codes or self.own_nick == presence['from'].resource
if own or len(self.presence_buffer) >= 10:
@@ -473,7 +479,7 @@ class MucTab(ChatTab):
Batch-process all the initial presences
"""
deterministic = config.get_by_tabname('deterministic_nick_colors',
- self.name)
+ self.jid.bare)
for stanza in self.presence_buffer:
try:
@@ -518,8 +524,8 @@ class MucTab(ChatTab):
self.own_nick = from_nick
self.own_user = new_user
self.joined = True
- if self.name in self.core.initial_joins:
- self.core.initial_joins.remove(self.name)
+ if self.jid.bare in self.core.initial_joins:
+ self.core.initial_joins.remove(self.jid.bare)
self._state = 'normal'
elif self != self.core.tabs.current_tab:
self._state = 'joined'
@@ -548,7 +554,7 @@ class MucTab(ChatTab):
'info_col': info_col,
}
self.add_message(enable_message, typ=2)
- self.core.enable_private_tabs(self.name, enable_message)
+ self.core.enable_private_tabs(self.jid.bare, enable_message)
if '201' in status_codes:
self.add_message(
'\x19%(info_col)s}Info: The room '
@@ -591,7 +597,7 @@ class MucTab(ChatTab):
self.on_user_join(from_nick, affiliation, show, status, role, jid,
user_color)
elif user is None:
- log.error('BUG: User %s in %s is None', from_nick, self.name)
+ log.error('BUG: User %s in %s is None', from_nick, self.jid.bare)
return
elif change_nick:
self.core.events.trigger('muc_nickchange', presence, self)
@@ -645,7 +651,7 @@ class MucTab(ChatTab):
When a new user joins the groupchat
"""
deterministic = config.get_by_tabname('deterministic_nick_colors',
- self.name)
+ self.jid.bare)
user = User(from_nick, affiliation, show, status, role, jid,
deterministic, color)
bisect.insort_left(self.users, user)
@@ -683,7 +689,7 @@ class MucTab(ChatTab):
'color_spec': spec_col,
}
self.add_message(msg, typ=2)
- self.core.on_user_rejoined_private_conversation(self.name, from_nick)
+ self.core.on_user_rejoined_private_conversation(self.jid.bare, from_nick)
def on_user_nick_change(self, presence, user, from_nick, from_room):
new_nick = presence.xml.find(
@@ -697,7 +703,7 @@ class MucTab(ChatTab):
else:
user.change_nick(new_nick)
deterministic = config.get_by_tabname('deterministic_nick_colors',
- self.name)
+ self.jid.bare)
color = config.get_by_tabname(new_nick, 'muc_colors') or None
if color or deterministic:
user.change_color(color, deterministic)
@@ -722,7 +728,7 @@ class MucTab(ChatTab):
},
typ=2)
# rename the private tabs if needed
- self.core.rename_private_tabs(self.name, from_nick, user)
+ self.core.rename_private_tabs(self.jid.bare, from_nick, user)
def on_user_banned(self, presence, user, from_nick):
"""
@@ -756,7 +762,7 @@ class MucTab(ChatTab):
'spec': char_kick,
'info_col': info_col
}
- self.core.disable_private_tabs(self.name, reason=kick_msg)
+ self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
self.disconnect()
self.refresh_tab_win()
self.core.tabs.current_tab.refresh_input()
@@ -765,11 +771,11 @@ class MucTab(ChatTab):
self.general_jid)
delay = common.parse_str_to_secs(delay)
if delay <= 0:
- muc.join_groupchat(self.core, self.name, self.own_nick)
+ muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
else:
self.core.add_timed_event(
timed_events.DelayedEvent(delay, muc.join_groupchat,
- self.core, self.name,
+ self.core, self.jid.bare,
self.own_nick))
else:
@@ -835,7 +841,7 @@ class MucTab(ChatTab):
'spec': char_kick,
'info_col': info_col
}
- self.core.disable_private_tabs(self.name, reason=kick_msg)
+ self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
self.disconnect()
self.refresh_tab_win()
self.core.tabs.current_tab.refresh_input()
@@ -845,11 +851,11 @@ class MucTab(ChatTab):
self.general_jid)
delay = common.parse_str_to_secs(delay)
if delay <= 0:
- muc.join_groupchat(self.core, self.name, self.own_nick)
+ muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
else:
self.core.add_timed_event(
timed_events.DelayedEvent(delay, muc.join_groupchat,
- self.core, self.name,
+ self.core, self.jid.bare,
self.own_nick))
else:
if config.get_by_tabname('display_user_color_in_join_part',
@@ -1037,7 +1043,7 @@ class MucTab(ChatTab):
to be
"""
if time is None and self.joined: # don't log the history messages
- if not logger.log_message(self.name, nickname, txt, typ=typ):
+ if not logger.log_message(self.jid.bare, nickname, txt, typ=typ):
self.core.information('Unable to write in the log file',
'Error')
@@ -1081,7 +1087,7 @@ class MucTab(ChatTab):
if (not time and nickname and nickname != self.own_nick
and self.state != 'current'):
if (self.state != 'highlight'
- and config.get_by_tabname('notify_messages', self.name)):
+ and config.get_by_tabname('notify_messages', self.jid.bare)):
self.state = 'message'
if time and not txt.startswith('/me'):
txt = '\x19%(info_col)s}%(txt)s' % {
@@ -1123,7 +1129,7 @@ class MucTab(ChatTab):
return False
def matching_names(self):
- return [(1, safeJID(self.name).user), (3, self.name)]
+ return [(1, self.jid.user), (3, self.jid.full)]
def enable_self_ping_event(self):
delay = config.get_by_tabname(
@@ -1146,7 +1152,7 @@ class MucTab(ChatTab):
def send_self_ping(self):
timeout = config.get_by_tabname(
"self_ping_timeout", self.general_jid, default=60)
- to = self.name + "/" + self.own_nick
+ to = self.jid.bare + "/" + self.own_nick
self.core.xmpp.plugin['xep_0199'].send_ping(
jid=to,
callback=self.on_self_ping_result,
@@ -1172,7 +1178,7 @@ class MucTab(ChatTab):
if color != '':
return color
nick_color_aliases = config.get_by_tabname('nick_color_aliases',
- self.name)
+ self.jid.bare)
if nick_color_aliases:
nick_alias = re.sub('^_*(.*?)_*$', '\\1', nick)
color = config.get_by_tabname(nick_alias, 'muc_colors')
@@ -1339,7 +1345,7 @@ class MucTab(ChatTab):
self.state = 'highlight'
beep_on = config.get('beep_on').split()
if 'highlight' in beep_on and 'message' not in beep_on:
- if not config.get_by_tabname('disable_beep', self.name):
+ if not config.get_by_tabname('disable_beep', self.jid.bare):
curses.beep()
return True
return False
@@ -1352,7 +1358,7 @@ class MucTab(ChatTab):
if args is None:
return self.core.command.help('invite')
jid, reason = args
- self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
+ self.core.command.invite('%s %s "%s"' % (jid, self.jid.bare, reason))
@command_args_parser.quoted(1)
def command_info(self, args):
@@ -1378,7 +1384,7 @@ class MucTab(ChatTab):
return
self.core.open_new_form(form, self.cancel_config, self.send_config)
- fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
+ fixes.get_room_form(self.core.xmpp, self.jid.bare, on_form_received)
@command_args_parser.raw
def command_cycle(self, msg):
@@ -1423,11 +1429,14 @@ class MucTab(ChatTab):
if args is None:
return self.core.command.help('version')
nick = args[0]
- if nick in [user.nick for user in self.users]:
- jid = safeJID(self.name).bare
- jid = safeJID(jid + '/' + nick)
- else:
- jid = safeJID(nick)
+ try:
+ if nick in [user.nick for user in self.users]:
+ jid = copy(self.jid)
+ jid.resource = nick
+ else:
+ jid = JID(nick)
+ except InvalidJID:
+ return self.core.information('Invalid jid or nick %r' % nick, 'Error')
self.core.xmpp.plugin['xep_0092'].get_version(
jid, callback=self.core.handler.on_version_result)
@@ -1443,9 +1452,12 @@ class MucTab(ChatTab):
return self.core.information('/nick only works in joined rooms',
'Info')
current_status = self.core.get_status()
- if not safeJID(self.name + '/' + nick):
+ try:
+ target_jid = copy(self.jid)
+ target_jid.resource = nick
+ except InvalidJID:
return self.core.information('Invalid nick', 'Info')
- muc.change_nick(self.core, self.name, nick, current_status.message,
+ muc.change_nick(self.core, self.jid.bare, nick, current_status.message,
current_status.show)
@command_args_parser.quoted(0, 1, [''])
@@ -1482,7 +1494,7 @@ class MucTab(ChatTab):
r = None
for user in self.users:
if user.nick == nick:
- r = self.core.open_private_window(self.name, user.nick)
+ r = self.core.open_private_window(self.jid.bare, user.nick)
if r and len(args) == 2:
msg = args[1]
self.core.tabs.current_tab.command_say(
@@ -1573,7 +1585,7 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
- self.core.room_error(iq, self.name)
+ self.core.room_error(iq, self.jid.bare)
if args is None:
return self.core.command.help('role')
@@ -1591,7 +1603,7 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
- self.core.room_error(iq, self.name)
+ self.core.room_error(iq, self.jid.bare)
if args is None:
return self.core.command.help('affiliation')
@@ -1606,7 +1618,7 @@ class MucTab(ChatTab):
Or normal input + enter
"""
needed = 'inactive' if self.inactive else 'active'
- msg = self.core.xmpp.make_message(self.name)
+ msg = self.core.xmpp.make_message(self.jid.bare)
msg['type'] = 'groupchat'
msg['body'] = line
# trigger the event BEFORE looking for colors.
@@ -1740,7 +1752,7 @@ class MucTab(ChatTab):
nicks = [
os.environ.get('USER'),
config.get('default_nick'),
- self.core.get_bookmark_nickname(self.name)
+ self.core.get_bookmark_nickname(self.jid.bare)
]
nicks = [i for i in nicks if i]
return Completion(the_input.auto_completion, nicks, '', quotify=False)
diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py
index cec68ac5..b4a64ba8 100644
--- a/poezio/tabs/privatetab.py
+++ b/poezio/tabs/privatetab.py
@@ -40,10 +40,9 @@ class PrivateTab(OneToOneTab):
message_type = 'chat'
additional_information = {} # type: Dict[str, Callable[[str], str]]
- def __init__(self, core, name, nick):
- OneToOneTab.__init__(self, core, name)
+ def __init__(self, core, jid, nick):
+ OneToOneTab.__init__(self, core, jid)
self.own_nick = nick
- self.name = name
self.text_win = windows.TextWin()
self._text_buffer.add_window(self.text_win)
self.info_header = windows.PrivateInfoWin()
@@ -64,24 +63,23 @@ class PrivateTab(OneToOneTab):
'Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
shortdesc='Get the software version of a jid.')
self.resize()
- self.parent_muc = self.core.tabs.by_name_and_class(
- safeJID(name).bare, MucTab)
+ self.parent_muc = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
self.on = True
self.update_commands()
self.update_keys()
def remote_user_color(self):
- user = self.parent_muc.get_user_by_name(safeJID(self.name).resource)
+ user = self.parent_muc.get_user_by_name(self.jid.resource)
if user:
return dump_tuple(user.color)
return super().remote_user_color()
@property
def general_jid(self):
- return self.name
+ return self.jid
def get_dest_jid(self):
- return self.name
+ return self.jid
@property
def nick(self):
@@ -90,10 +88,11 @@ class PrivateTab(OneToOneTab):
def ack_message(self, msg_id: str, msg_jid: JID):
# special case when talking to oneself
if msg_jid == self.core.xmpp.boundjid:
- msg_jid = JID(self.name)
+ msg_jid = self.jid.full
super().ack_message(msg_id, msg_jid)
@staticmethod
+ @refresh_wrapper.always
def add_information_element(plugin_name, callback):
"""
Lets a plugin add its own information to the PrivateInfoWin
@@ -101,12 +100,12 @@ class PrivateTab(OneToOneTab):
PrivateTab.additional_information[plugin_name] = callback
@staticmethod
+ @refresh_wrapper.always
def remove_information_element(plugin_name):
del PrivateTab.additional_information[plugin_name]
def load_logs(self, log_nb):
- logs = logger.get_logs(
- safeJID(self.name).full.replace('/', '\\'), log_nb)
+ logs = logger.get_logs(self.jid.full.replace('/', '\\'), log_nb)
return logs
def log_message(self, txt, nickname, time=None, typ=1):
@@ -114,7 +113,7 @@ class PrivateTab(OneToOneTab):
Log the messages in the archives.
"""
if not logger.log_message(
- self.name, nickname, txt, date=time, typ=typ):
+ self.jid.full, nickname, txt, date=time, typ=typ):
self.core.information('Unable to write in the log file', 'Error')
def on_close(self):
@@ -149,8 +148,8 @@ class PrivateTab(OneToOneTab):
def command_say(self, line, attention=False, correct=False):
if not self.on:
return
- echo_message = JID(self.name).resource != self.own_nick
- msg = self.core.xmpp.make_message(self.name)
+ echo_message = self.jid.resource != self.own_nick
+ msg = self.core.xmpp.make_message(self.jid.full)
msg['type'] = 'chat'
msg['body'] = line
# trigger the event BEFORE looking for colors.
@@ -166,7 +165,7 @@ class PrivateTab(OneToOneTab):
replaced = False
if correct or msg['replace']['id']:
msg['replace']['id'] = self.last_sent_message['id']
- if (config.get_by_tabname('group_corrections', self.name)
+ if (config.get_by_tabname('group_corrections', self.jid.full)
and echo_message):
try:
self.modify_message(
@@ -221,7 +220,7 @@ class PrivateTab(OneToOneTab):
"""
if args:
return self.core.command.version(args[0])
- jid = safeJID(self.name)
+ jid = self.jid.full
self.core.xmpp.plugin['xep_0092'].get_version(
jid, callback=self.core.handler.on_version_result)
@@ -233,7 +232,7 @@ class PrivateTab(OneToOneTab):
if arg and arg[0]:
self.parent_muc.command_info(arg[0])
else:
- user = safeJID(self.name).resource
+ user = self.jid.resource
self.parent_muc.command_info(user)
def resize(self):
@@ -262,7 +261,7 @@ class PrivateTab(OneToOneTab):
display_info_win = not self.size.tab_degrade_y
self.text_win.refresh()
- self.info_header.refresh(self.name, self.text_win, self.chatstate,
+ self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
PrivateTab.additional_information)
if display_info_win:
self.info_win.refresh()
@@ -271,12 +270,12 @@ class PrivateTab(OneToOneTab):
self.input.refresh()
def refresh_info_header(self):
- self.info_header.refresh(self.name, self.text_win, self.chatstate,
+ self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
PrivateTab.additional_information)
self.input.refresh()
def get_nick(self):
- return safeJID(self.name).resource
+ return self.jid.resource
def on_input(self, key, raw):
if not raw and key in self.key_func:
@@ -288,7 +287,7 @@ class PrivateTab(OneToOneTab):
empty_after = self.input.get_text() == '' or (
self.input.get_text().startswith('/')
and not self.input.get_text().startswith('//'))
- tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
+ tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
if tab and tab.joined:
self.send_composing_chat_state(empty_after)
return False
@@ -301,7 +300,7 @@ class PrivateTab(OneToOneTab):
self.text_win.remove_line_separator()
self.text_win.add_line_separator(self._text_buffer)
- tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
+ tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
if tab and tab.joined and config.get_by_tabname(
'send_chat_states', self.general_jid) and self.on:
self.send_chat_state('inactive')
@@ -310,7 +309,7 @@ class PrivateTab(OneToOneTab):
def on_gain_focus(self):
self.state = 'current'
curses.curs_set(1)
- tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
+ tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
if tab and tab.joined and config.get_by_tabname(
'send_chat_states',
self.general_jid,
@@ -345,7 +344,7 @@ class PrivateTab(OneToOneTab):
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
},
typ=2)
- new_jid = safeJID(self.name).bare + '/' + user.nick
+ new_jid = self.jid.bare + '/' + user.nick
self.name = new_jid
return self.core.tabs.current_tab is self
@@ -426,7 +425,7 @@ class PrivateTab(OneToOneTab):
self.add_message(txt=reason, typ=2)
def matching_names(self):
- return [(3, safeJID(self.name).resource), (4, self.name)]
+ return [(3, self.jid.resource), (4, self.name)]
def add_error(self, error_message):
theme = get_theme()
diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py
index 7c941aa9..a5ce268b 100644
--- a/poezio/tabs/rostertab.py
+++ b/poezio/tabs/rostertab.py
@@ -92,22 +92,6 @@ class RosterInfoTab(Tab):
shortdesc='Deny a user your presence.',
completion=self.completion_deny)
self.register_command(
- 'accept',
- self.command_accept,
- usage='[jid]',
- desc='Allow the provided JID (or the selected contact '
- 'in your roster), to see your presence.',
- shortdesc='Allow a user your presence.',
- completion=self.completion_deny)
- self.register_command(
- 'add',
- self.command_add,
- usage='<jid>',
- desc='Add the specified JID to your roster, ask them to'
- ' allow you to see his presence, and allow them to'
- ' see your presence.',
- shortdesc='Add a user to your roster.')
- self.register_command(
'name',
self.command_name,
usage='<jid> [name]',
@@ -692,27 +676,6 @@ class RosterInfoTab(Tab):
'Roster')
@deny_anonymous
- @command_args_parser.quoted(1)
- def command_add(self, args):
- """
- Add the specified JID to the roster, and automatically
- accept the reverse subscription
- """
- if args is None:
- self.core.information('No JID specified', 'Error')
- return
- jid = safeJID(safeJID(args[0]).bare)
- if not str(jid):
- self.core.information(
- 'The provided JID (%s) is not valid' % (args[0], ), 'Error')
- return
- if jid in roster and roster[jid].subscription in ('to', 'both'):
- return self.core.information('Already subscribed.', 'Roster')
- roster.add(jid)
- roster.modified()
- self.core.information('%s was added to the roster' % jid, 'Roster')
-
- @deny_anonymous
@command_args_parser.quoted(1, 1)
def command_name(self, args):
"""
@@ -951,7 +914,7 @@ class RosterInfoTab(Tab):
log.error('Unable to correct a message', exc_info=True)
return
for jid in lines:
- self.command_add(jid.lstrip('\n'))
+ self.command.command_add(jid.lstrip('\n'))
self.core.information('Contacts imported from %s' % filepath, 'Info')
@deny_anonymous
@@ -1059,40 +1022,6 @@ class RosterInfoTab(Tab):
if contact.pending_in)
return Completion(the_input.new_completion, jids, 1, '', quotify=False)
- @deny_anonymous
- @command_args_parser.quoted(0, 1)
- def command_accept(self, args):
- """
- Accept a JID from in roster. Authorize it AND subscribe to it
- """
- if not args:
- item = self.roster_win.selected_row
- if isinstance(item, Contact):
- jid = item.bare_jid
- else:
- self.core.information('No subscription to accept', 'Warning')
- return
- else:
- jid = safeJID(args[0]).bare
- nodepart = safeJID(jid).user
- jid = safeJID(jid)
- # crappy transports putting resources inside the node part
- if '\\2f' in nodepart:
- jid.user = nodepart.split('\\2f')[0]
- contact = roster[jid]
- if contact is None:
- return
- contact.pending_in = False
- roster.modified()
- self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
- self.core.xmpp.client_roster.send_last_presence()
- if contact.subscription in ('from',
- 'none') and not contact.pending_out:
- self.core.xmpp.send_presence(
- pto=jid, ptype='subscribe', pnick=self.core.own_nick)
-
- self.core.information('%s is now authorized' % jid, 'Roster')
-
def refresh(self):
if self.need_resize:
self.resize()
diff --git a/poezio/windows/image.py b/poezio/windows/image.py
index dfd2eae2..ebecb5ad 100644
--- a/poezio/windows/image.py
+++ b/poezio/windows/image.py
@@ -20,7 +20,7 @@ try:
from gi.repository import Rsvg
import cairo
HAS_RSVG = True
-except (ImportError, ValueError):
+except (ImportError, ValueError, AttributeError):
HAS_RSVG = False
from poezio.windows.base_wins import Win
diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py
index 1de905ea..96161d51 100644
--- a/poezio/windows/text_win.py
+++ b/poezio/windows/text_win.py
@@ -267,8 +267,8 @@ class TextWin(BaseTextWin):
def scroll_to_separator(self) -> None:
"""
- Scroll until separator is centered. If no separator is
- present, scroll at the top of the window
+ Scroll to the first message after the separator. If no
+ separator is present, scroll to the first message of the window
"""
if None in self.built_lines:
self.pos = len(self.built_lines) - self.built_lines.index(