diff options
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( |