summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/admin.py8
-rw-r--r--plugins/alias.py4
-rw-r--r--plugins/amsg.py6
-rw-r--r--plugins/b64.py70
-rw-r--r--plugins/bob.py6
-rw-r--r--plugins/code.py10
-rw-r--r--plugins/contact.py56
-rw-r--r--plugins/day_change.py9
-rw-r--r--plugins/dice.py39
-rw-r--r--plugins/disco.py59
-rw-r--r--plugins/display_corrections.py12
-rw-r--r--plugins/embed.py32
-rw-r--r--plugins/emoji_ascii.py60
-rw-r--r--plugins/exec.py2
-rw-r--r--plugins/figlet.py22
-rw-r--r--plugins/irc.py246
-rw-r--r--plugins/lastlog.py61
-rw-r--r--plugins/link.py17
-rw-r--r--plugins/marquee.py15
-rw-r--r--plugins/mirror.py2
-rw-r--r--plugins/mpd_client.py2
-rw-r--r--plugins/otr.py151
-rw-r--r--plugins/ping.py57
-rwxr-xr-xplugins/qr.py184
-rw-r--r--plugins/quote.py18
-rw-r--r--plugins/rainbow.py2
-rw-r--r--plugins/remove_get_trackers.py24
-rw-r--r--plugins/reorder.py60
-rw-r--r--plugins/replace.py8
-rw-r--r--plugins/screen_detach.py4
-rw-r--r--plugins/send_delayed.py5
-rw-r--r--plugins/server_part.py16
-rw-r--r--plugins/simple_notify.py5
-rw-r--r--plugins/sticker.py97
-rw-r--r--plugins/stoi.py2
-rw-r--r--plugins/tell.py5
-rw-r--r--plugins/time_marker.py10
-rw-r--r--plugins/untrackme.py140
-rw-r--r--plugins/upload.py42
-rw-r--r--plugins/uptime.py30
-rw-r--r--plugins/user_extras.py634
-rw-r--r--plugins/vcard.py48
42 files changed, 1846 insertions, 434 deletions
diff --git a/plugins/admin.py b/plugins/admin.py
index 7bbc01d6..c2901844 100644
--- a/plugins/admin.py
+++ b/plugins/admin.py
@@ -122,10 +122,14 @@ class Plugin(BasePlugin):
completion=self.complete_nick)
def role(self, role):
- return lambda args: self.api.current_tab().command_role(args + ' ' + role)
+ async def inner(args):
+ await self.api.current_tab().command_role(args + ' ' + role)
+ return inner
def affiliation(self, affiliation):
- return lambda args: self.api.current_tab().command_affiliation(args + ' ' + affiliation)
+ async def inner(args):
+ await self.api.current_tab().command_affiliation(args + ' ' + affiliation)
+ return inner
def complete_nick(self, the_input):
tab = self.api.current_tab()
diff --git a/plugins/alias.py b/plugins/alias.py
index a10beb7c..459ce02c 100644
--- a/plugins/alias.py
+++ b/plugins/alias.py
@@ -128,7 +128,7 @@ class Plugin(BasePlugin):
if update:
self.api.information('Alias /%s updated' % alias, 'Info')
else:
- self.api.information('Alias /%s successfuly created' % alias,
+ self.api.information('Alias /%s successfully created' % alias,
'Info')
def command_unalias(self, alias):
@@ -139,7 +139,7 @@ class Plugin(BasePlugin):
del self.commands[alias]
self.api.del_command(alias)
self.config.remove(alias)
- self.api.information('Alias /%s successfuly deleted' % alias,
+ self.api.information('Alias /%s successfully deleted' % alias,
'Info')
def completion_unalias(self, the_input):
diff --git a/plugins/amsg.py b/plugins/amsg.py
index b8ac4e26..3b81085a 100644
--- a/plugins/amsg.py
+++ b/plugins/amsg.py
@@ -1,7 +1,7 @@
"""
This plugin broadcasts a message to all your joined rooms.
-.. note:: With great power comes great responsability.
+.. note:: With great power comes great responsibility.
Use with moderation.
Command
@@ -29,7 +29,7 @@ class Plugin(BasePlugin):
short='Broadcast a message',
help='Broadcast the message to all the joined rooms.')
- def command_amsg(self, args):
+ async def command_amsg(self, args):
for room in self.core.tabs:
if isinstance(room, MucTab) and room.joined:
- room.command_say(args)
+ await room.command_say(args)
diff --git a/plugins/b64.py b/plugins/b64.py
new file mode 100644
index 00000000..82300a0f
--- /dev/null
+++ b/plugins/b64.py
@@ -0,0 +1,70 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# Copyright © 2019 Maxime “pep” Buquet <pep@bouah.net>
+#
+# Distributed under terms of the GPL-3.0+ 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 typing import List, Optional
+from slixmpp import Message, JID
+
+from poezio.plugin_e2ee import E2EEPlugin
+from poezio.tabs import (
+ ChatTab,
+ MucTab,
+ PrivateTab,
+ DynamicConversationTab,
+ StaticConversationTab,
+)
+
+
+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
+
+ # In what tab is it ok to use this plugin. Here we want all of them
+ supported_tab_types = (
+ MucTab,
+ PrivateTab,
+ DynamicConversationTab,
+ StaticConversationTab,
+ )
+
+ async def decrypt(self, message: Message, jid: Optional[JID], _tab: Optional[ChatTab]) -> None:
+ """
+ Decrypt base64
+ """
+ body = message['body']
+ message['body'] = b64decode(body.encode()).decode()
+
+ async def encrypt(self, message: Message, _jid: Optional[List[JID]], _tab: ChatTab) -> 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..98c62901 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',
@@ -47,7 +47,7 @@ class Plugin(BasePlugin):
short='Send a short image',
completion=self.completion_bob)
- def command_bob(self, filename):
+ async def command_bob(self, filename):
path = Path(expanduser(filename))
try:
size = path.stat().st_size
@@ -67,7 +67,7 @@ class Plugin(BasePlugin):
with open(path.as_posix(), 'rb') as file:
data = file.read()
max_age = self.config.get('max_age')
- cid = self.core.xmpp.plugin['xep_0231'].set_bob(
+ cid = await self.core.xmpp.plugin['xep_0231'].set_bob(
data, mime_type, max_age=max_age)
self.api.run_command(
'/xhtml <img src="cid:%s" alt="%s"/>' % (cid, path.name))
diff --git a/plugins/code.py b/plugins/code.py
index fa04f758..8d9c57a3 100644
--- a/plugins/code.py
+++ b/plugins/code.py
@@ -41,8 +41,12 @@ class Plugin(BasePlugin):
help='Sends syntax-highlighted code in the current tab')
def command_code(self, args):
- language, code = args.split(None, 1)
+ split = args.split(None, 1)
+ if len(split) != 2:
+ self.api.information('Usage: /code <language> <code>', 'Error')
+ return None
+ language, code = split
lexer = get_lexer_by_name(language)
- room = self.api.current_tab()
+ tab = self.api.current_tab()
code = highlight(code, lexer, FORMATTER)
- room.command_xhtml('<pre>%s</pre>' % code.rstrip('\n'))
+ tab.command_xhtml('<pre><code class="language-%s">%s</code></pre>' % (language, code.rstrip('\n')))
diff --git a/plugins/contact.py b/plugins/contact.py
index ebe4dcc4..13dcc42f 100644
--- a/plugins/contact.py
+++ b/plugins/contact.py
@@ -13,6 +13,7 @@ Usage
"""
from poezio.plugin import BasePlugin
+from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.jid import InvalidJID
CONTACT_TYPES = ['abuse', 'admin', 'feedback', 'sales', 'security', 'support']
@@ -25,38 +26,35 @@ class Plugin(BasePlugin):
help='Get the Contact Addresses of a JID')
def on_disco(self, iq):
- if iq['type'] == 'error':
- error_condition = iq['error']['condition']
- error_text = iq['error']['text']
- message = 'Error getting Contact Addresses from %s: %s: %s' % (iq['from'], error_condition, error_text)
- self.api.information(message, 'Error')
- return
info = iq['disco_info']
- title = 'Contact Info'
contacts = []
- for field in info['form']:
- var = field['var']
- if field['type'] == 'hidden' and var == 'FORM_TYPE':
- form_type = field['value'][0]
- if form_type != 'http://jabber.org/network/serverinfo':
- self.api.information('Not a server: “%s”: %s' % (iq['from'], form_type), 'Error')
- return
- continue
- if not var.endswith('-addresses'):
- continue
- var = var[:-10] # strip '-addresses'
- sep = '\n ' + len(var) * ' '
- field_value = field.get_value(convert=False)
- value = sep.join(field_value) if isinstance(field_value, list) else field_value
- contacts.append('%s: %s' % (var, value))
+ # iterate all data forms, in case there are multiple
+ for form in iq['disco_info']:
+ values = form.get_values()
+ if values['FORM_TYPE'][0] == 'http://jabber.org/network/serverinfo':
+ for var in values:
+ if not var.endswith('-addresses'):
+ continue
+ title = var[:-10] # strip '-addresses'
+ sep = '\n ' + len(title) * ' '
+ field_value = values[var]
+ if field_value:
+ value = sep.join(field_value) if isinstance(field_value, list) else field_value
+ contacts.append(f'{title}: {value}')
if contacts:
- self.api.information('\n'.join(contacts), title)
+ self.api.information('\n'.join(contacts), 'Contact Info')
else:
- self.api.information('No Contact Addresses for %s' % iq['from'], 'Error')
+ self.api.information(f'No Contact Addresses for {iq["from"]}', 'Error')
- def command_disco(self, jid):
+ async def command_disco(self, jid):
try:
- self.core.xmpp.plugin['xep_0030'].get_info(jid=jid, cached=False,
- callback=self.on_disco)
- except InvalidJID as e:
- self.api.information('Invalid JID “%s”: %s' % (jid, e), 'Error')
+ iq = await self.core.xmpp.plugin['xep_0030'].get_info(jid=jid, cached=False)
+ self.on_disco(iq)
+ except InvalidJID as exn:
+ self.api.information(f'Invalid JID “{jid}”: {exn}', 'Error')
+ except (IqError, IqTimeout,) as exn:
+ ifrom = exn.iq['from']
+ condition = exn.iq['error']['condition']
+ text = exn.iq['error']['text']
+ message = f'Error getting Contact Addresses from {ifrom}: {condition}: {text}'
+ self.api.information(message, 'Error')
diff --git a/plugins/day_change.py b/plugins/day_change.py
index 051b447b..5d3ab37c 100644
--- a/plugins/day_change.py
+++ b/plugins/day_change.py
@@ -4,11 +4,12 @@ date has changed.
"""
+import datetime
from gettext import gettext as _
+
+from poezio import timed_events, tabs
from poezio.plugin import BasePlugin
-import datetime
-from poezio import tabs
-from poezio import timed_events
+from poezio.ui.types import InfoMessage
class Plugin(BasePlugin):
@@ -30,7 +31,7 @@ class Plugin(BasePlugin):
for tab in self.core.tabs:
if isinstance(tab, tabs.ChatTab):
- tab.add_message(msg)
+ tab.add_message(InfoMessage(msg))
self.core.refresh_window()
self.schedule_event()
diff --git a/plugins/dice.py b/plugins/dice.py
index 376ed26a..3b540cbd 100644
--- a/plugins/dice.py
+++ b/plugins/dice.py
@@ -29,6 +29,7 @@ Configuration
"""
import random
+from typing import Optional
from poezio import tabs
from poezio.decorators import command_args_parser
@@ -40,17 +41,16 @@ DICE = '\u2680\u2681\u2682\u2683\u2684\u2685'
class DiceRoll:
__slots__ = [
'duration', 'total_duration', 'dice_number', 'msgtype', 'jid',
- 'last_msgid', 'increments'
+ 'msgid', 'increments'
]
- def __init__(self, total_duration, dice_number, is_muc, jid, msgid,
- increments):
+ def __init__(self, total_duration, dice_number, msgtype, jid, msgid, increments):
self.duration = 0
self.total_duration = total_duration
self.dice_number = dice_number
- self.msgtype = "groupchat" if is_muc else "chat"
+ self.msgtype = msgtype
self.jid = jid
- self.last_msgid = msgid
+ self.msgid = msgid
self.increments = increments
def reroll(self):
@@ -60,11 +60,14 @@ class DiceRoll:
return self.duration >= self.total_duration
+def roll_dice(num_dice: int) -> str:
+ return ''.join(random.choice(DICE) for _ in range(num_dice))
+
class Plugin(BasePlugin):
- default_config = {"dice": {"refresh": 0.5, "default_duration": 5}}
+ default_config = {"dice": {"refresh": 0.75, "default_duration": 7.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',
@@ -90,13 +93,17 @@ class Plugin(BasePlugin):
self.core.command.help("roll")
return
- firstroll = ''.join(random.choice(DICE) for _ in range(num_dice))
- tab.command_say(firstroll)
- is_muctab = isinstance(tab, tabs.MucTab)
- msg_id = tab.last_sent_message["id"]
+ msgtype = 'groupchat' if isinstance(tab, tabs.MucTab) else 'chat'
+
+ message = self.core.xmpp.make_message(tab.jid)
+ message['type'] = msgtype
+ message['body'] = roll_dice(num_dice)
+ message.send()
+
increment = self.config.get('refresh')
- roll = DiceRoll(duration, num_dice, is_muctab, tab.name, msg_id,
- increment)
+ msgid = message['id']
+
+ roll = DiceRoll(duration, num_dice, msgtype, tab.jid, msgid, increment)
event = self.api.create_delayed_event(increment, self.delayed_event,
roll)
self.api.add_timed_event(event)
@@ -107,11 +114,9 @@ class Plugin(BasePlugin):
roll.reroll()
message = self.core.xmpp.make_message(roll.jid)
message["type"] = roll.msgtype
- message["body"] = ''.join(
- random.choice(DICE) for _ in range(roll.dice_number))
- message["replace"]["id"] = roll.last_msgid
+ message["body"] = roll_dice(roll.dice_number)
+ message["replace"]["id"] = roll.msgid
message.send()
- roll.last_msgid = message['id']
event = self.api.create_delayed_event(roll.increments,
self.delayed_event, roll)
self.api.add_timed_event(event)
diff --git a/plugins/disco.py b/plugins/disco.py
index f6769146..d15235f6 100644
--- a/plugins/disco.py
+++ b/plugins/disco.py
@@ -16,7 +16,9 @@ Usage
"""
from poezio.plugin import BasePlugin
+from poezio.decorators import command_args_parser
from slixmpp.jid import InvalidJID
+from slixmpp.exceptions import IqError, IqTimeout
class Plugin(BasePlugin):
@@ -24,11 +26,15 @@ class Plugin(BasePlugin):
self.api.add_command(
'disco',
self.command_disco,
- usage='<JID>',
+ usage='<JID> [node] [info|items]',
short='Get the disco#info of a JID',
help='Get the disco#info of a JID')
- def on_disco(self, iq):
+ def on_info(self, iq):
+ if iq['type'] == 'error':
+ self.api.information(iq['error']['text'] or iq['error']['condition'], 'Error')
+ return
+
info = iq['disco_info']
identities = (str(identity) for identity in info['identities'])
self.api.information('\n'.join(identities), 'Identities')
@@ -49,9 +55,52 @@ class Plugin(BasePlugin):
if server_info:
self.api.information('\n'.join(server_info), title)
- def command_disco(self, jid):
+ def on_items(self, iq):
+ if iq['type'] == 'error':
+ self.api.information(iq['error']['text'] or iq['error']['condition'], 'Error')
+ return
+
+ def describe(item):
+ text = item[0]
+ node = item[1]
+ name = item[2]
+ if node is not None:
+ text += ', node=' + node
+ if name is not None:
+ text += ', name=' + name
+ return text
+
+ items = iq['disco_items']
+ self.api.information('\n'.join(describe(item) for item in items['items']), 'Items')
+
+ @command_args_parser.quoted(1, 3)
+ async def command_disco(self, args):
+ if args is None:
+ self.core.command.help('disco')
+ return
+ if len(args) == 1:
+ jid, = args
+ node = None
+ type_ = 'info'
+ elif len(args) == 2:
+ jid, node = args
+ type_ = 'info'
+ else:
+ jid, node, type_ = args
try:
- self.core.xmpp.plugin['xep_0030'].get_info(
- jid=jid, cached=False, callback=self.on_disco)
+ if type_ == 'info':
+ iq = await self.core.xmpp.plugin['xep_0030'].get_info(
+ jid=jid, node=node, cached=False
+ )
+ self.on_info(iq)
+ elif type_ == 'items':
+ iq = await self.core.xmpp.plugin['xep_0030'].get_items(
+ jid=jid, node=node
+ )
+ self.on_items(iq)
except InvalidJID as e:
self.api.information('Invalid JID “%s”: %s' % (jid, e), 'Error')
+ except IqError as e:
+ self.api.information('Received iq error while querying “%s”: %s' % (jid, e), 'Error')
+ except IqTimeout:
+ self.api.information('Received no reply querying “%s”…' % jid, 'Error')
diff --git a/plugins/display_corrections.py b/plugins/display_corrections.py
index 22eb196d..cf8107ce 100644
--- a/plugins/display_corrections.py
+++ b/plugins/display_corrections.py
@@ -25,11 +25,13 @@ Usage
from poezio.plugin import BasePlugin
from poezio.common import shell_split
from poezio import tabs
+from poezio.ui.types import Message
+from poezio.theming import get_theme
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',
@@ -43,7 +45,9 @@ class Plugin(BasePlugin):
messages = self.api.get_conversation_messages()
if not messages:
return None
- for message in messages[::-1]:
+ for message in reversed(messages):
+ if not isinstance(message, Message):
+ continue
if message.old_message:
if nb == 1:
return message
@@ -52,6 +56,7 @@ class Plugin(BasePlugin):
return None
def command_display_corrections(self, args):
+ theme = get_theme()
args = shell_split(args)
if len(args) == 1:
try:
@@ -64,8 +69,9 @@ class Plugin(BasePlugin):
if message:
display = []
while message:
+ str_time = message.time.strftime(theme.SHORT_TIME_FORMAT)
display.append('%s %s%s%s %s' %
- (message.str_time, '* '
+ (str_time, '* '
if message.me else '', message.nickname, ''
if message.me else '>', message.txt))
message = message.old_message
diff --git a/plugins/embed.py b/plugins/embed.py
index 726b1eb2..4a68f035 100644
--- a/plugins/embed.py
+++ b/plugins/embed.py
@@ -16,11 +16,12 @@ Usage
from poezio import tabs
from poezio.plugin import BasePlugin
from poezio.theming import get_theme
+from poezio.ui.types import Message
class Plugin(BasePlugin):
def init(self):
- for tab_t in [tabs.MucTab, tabs.ConversationTab, tabs.PrivateTab]:
+ for tab_t in [tabs.MucTab, tabs.StaticConversationTab, tabs.DynamicConversationTab, tabs.PrivateTab]:
self.api.add_tab_command(
tab_t,
'embed',
@@ -28,21 +29,22 @@ class Plugin(BasePlugin):
help='Embed an image url into the contact\'s client',
usage='<image_url>')
- def embed_image_url(self, args):
- tab = self.api.current_tab()
- message = self.core.xmpp.make_message(tab.name)
- message['body'] = args
- message['oob']['url'] = args
- if isinstance(tab, tabs.MucTab):
- message['type'] = 'groupchat'
- else:
+ def embed_image_url(self, url, tab=None):
+ tab = tab or self.api.current_tab()
+ message = self.core.xmpp.make_message(tab.jid)
+ message['body'] = url
+ message['oob']['url'] = url
+ message['type'] = 'groupchat'
+ if not isinstance(tab, tabs.MucTab):
message['type'] = 'chat'
tab.add_message(
- message['body'],
- nickname=tab.core.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=message['id'],
- jid=tab.core.xmpp.boundjid,
- typ=1,
+ Message(
+ message['body'],
+ nickname=tab.core.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=message['id'],
+ jid=tab.core.xmpp.boundjid,
+ ),
)
message.send()
+ self.core.refresh_window()
diff --git a/plugins/emoji_ascii.py b/plugins/emoji_ascii.py
new file mode 100644
index 00000000..4beec3b1
--- /dev/null
+++ b/plugins/emoji_ascii.py
@@ -0,0 +1,60 @@
+# 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
+from typing import Dict
+
+
+class Plugin(BasePlugin):
+ emoji_to_ascii: Dict[str, str] = {}
+ ascii_to_emoji: Dict[str, str] = {}
+ 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/exec.py b/plugins/exec.py
index 0786c86f..68f24486 100644
--- a/plugins/exec.py
+++ b/plugins/exec.py
@@ -95,4 +95,4 @@ class Plugin(BasePlugin):
else:
self.api.run_command('/help exec')
return
- asyncio.ensure_future(self.async_exec(command, arg))
+ asyncio.create_task(self.async_exec(command, arg))
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..f3aa7b63 100644
--- a/plugins/irc.py
+++ b/plugins/irc.py
@@ -20,9 +20,9 @@ Global configuration
:sorted:
gateway
- **Default:** ``irc.poez.io``
+ **Default:** ``irc.jabberfr.org``
- The JID of the IRC gateway to use. If empty, irc.poez.io will be
+ The JID of the IRC gateway to use. If empty, irc.jabberfr.org will be
used. Please try to run your own, though, it’s painless to setup.
initial_connect
@@ -46,17 +46,6 @@ section name, and the following options:
.. glossary::
:sorted:
-
- login_command
- **Default:** ``[empty]``
-
- The command used to identify with the services (e.g. ``IDENTIFY mypassword``).
-
- login_nick
- **Default:** ``[empty]``
-
- The nickname to whom the auth command will be sent.
-
nickname
**Default:** ``[empty]``
@@ -77,14 +66,6 @@ Commands
.. glossary::
:sorted:
- /irc_login
- **Usage:** ``/irc_login [server1] [server2]…``
-
- Authenticate with the specified servers if they are correctly
- configured. If no servers are provided, the plugin will try
- them all. (You need to set :term:`login_nick` and
- :term:`login_command` as well)
-
/irc_join
**Usage:** ``/irc_join <room or server>``
@@ -109,9 +90,9 @@ Example configuration
.. code-block:: ini
[irc]
- gateway = irc.poez.io
+ gateway = irc.jabberfr.org
- [irc.freenode.net]
+ [irc.libera.chat]
nickname = mynick
login_nick = nickserv
login_command = identify mypassword
@@ -129,30 +110,30 @@ Example configuration
"""
+import asyncio
+
+from typing import Optional, Tuple, List, Any
+from slixmpp.jid import JID, InvalidJID
+
from poezio.plugin import BasePlugin
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
-from poezio import common
from poezio import tabs
class Plugin(BasePlugin):
- def init(self):
- if self.config.get('initial_connect', True):
- self.initial_connect()
-
- self.api.add_command(
- 'irc_login',
- self.command_irc_login,
- usage='[server] [server]…',
- help=('Connect to the specified servers if they '
- 'exist in the configuration and the login '
- 'options are set. If not is given, the '
- 'plugin will try all the sections in the '
- 'configuration.'),
- short='Login to irc servers with nickserv',
- completion=self.completion_irc_login)
-
+ default_config = {
+ 'irc': {
+ "initial_connect": True,
+ "gateway": "irc.jabberfr.org",
+ }
+ }
+
+ def init(self) -> None:
+ if self.config.getbool('initial_connect'):
+ asyncio.create_task(
+ self.initial_connect()
+ )
self.api.add_command(
'irc_join',
self.command_irc_join,
@@ -179,22 +160,38 @@ class Plugin(BasePlugin):
'example.com "hi there"`'),
short='Open a private conversation with an IRC user')
- def join(self, gateway, server):
+ async def join(self, gateway: str, server: JID) -> None:
"Join irc rooms on a server"
- nick = self.config.get_by_tabname(
+ nick: str = self.config.get_by_tabname(
'nickname', server, default='') or self.core.own_nick
- rooms = self.config.get_by_tabname(
+ rooms: List[str] = self.config.get_by_tabname(
'rooms', server, default='').split(':')
+ joins = []
for room in rooms:
room = '{}%{}@{}/{}'.format(room, server, gateway, nick)
- self.core.command.join(room)
+ joins.append(self.core.command.join(room))
- def initial_connect(self):
- gateway = self.config.get('gateway', 'irc.poez.io')
- sections = self.config.sections()
+ await asyncio.gather(*joins)
- for section in (s for s in sections if s != 'irc'):
+ async def initial_connect(self) -> None:
+ gateway: str = self.config.getstr('gateway')
+ sections: List[str] = self.config.sections()
+ sections_jid = []
+ for sect in sections:
+ if sect == 'irc':
+ continue
+ try:
+ sect_jid = JID(sect)
+ if sect_jid != sect_jid.server:
+ self.api.information(f'Invalid server: {sect}', 'Warning')
+ continue
+ except InvalidJID:
+ self.api.information(f'Invalid server: {sect}', 'Warning')
+ continue
+ sections_jid.append(sect_jid)
+
+ for section in sections_jid:
room_suffix = '%{}@{}'.format(section, gateway)
already_opened = False
@@ -203,125 +200,40 @@ class Plugin(BasePlugin):
already_opened = True
break
- login_command = self.config.get_by_tabname(
- 'login_command', section, default='')
- login_nick = self.config.get_by_tabname(
- 'login_nick', section, default='')
- nick = self.config.get_by_tabname(
- 'nickname', section, default='') or self.core.own_nick
- if login_command and login_nick:
-
- def login(gw, sect, log_nick, log_cmd, room_suff):
- dest = '{}%{}'.format(log_nick, room_suff)
- self.core.xmpp.send_message(
- mto=dest, mbody=log_cmd, mtype='chat')
- delayed = self.api.create_delayed_event(
- 5, self.join, gw, sect)
- self.api.add_timed_event(delayed)
-
- if not already_opened:
- self.core.command.join(room_suffix + '/' + nick)
- delayed = self.api.create_delayed_event(
- 5, login, gateway, section, login_nick, login_command,
- room_suffix[1:])
- self.api.add_timed_event(delayed)
- else:
- login(gateway, section, login_nick, login_command,
- room_suffix[1:])
- elif not already_opened:
- self.join(gateway, section)
-
- @command_args_parser.quoted(0, -1)
- def command_irc_login(self, args):
- """
- /irc_login [server] [server]…
- """
- gateway = self.config.get('gateway', 'irc.poez.io')
- if args:
- not_present = []
- sections = self.config.sections()
- for section in args:
- if section not in sections:
- not_present.append(section)
- continue
- login_command = self.config.get_by_tabname(
- 'login_command', section, default='')
- login_nick = self.config.get_by_tabname(
- 'login_nick', section, default='')
- if not login_command and not login_nick:
- not_present.append(section)
- continue
-
- room_suffix = '%{}@{}'.format(section, gateway)
- dest = '{}%{}'.format(login_nick, room_suffix[1:])
- self.core.xmpp.send_message(
- mto=dest, mbody=login_command, mtype='chat')
- if len(not_present) == 1:
- self.api.information(
- 'Section %s does not exist or is not configured' %
- not_present[0], 'Warning')
- elif len(not_present) > 1:
- self.api.information(
- 'Sections %s do not exist or are not configured' %
- ', '.join(not_present), 'Warning')
- else:
- sections = self.config.sections()
-
- for section in (s for s in sections if s != 'irc'):
- login_command = self.config.get_by_tabname(
- 'login_command', section, default='')
- login_nick = self.config.get_by_tabname(
- 'login_nick', section, default='')
- if not login_nick and not login_command:
- continue
-
- room_suffix = '%{}@{}'.format(section, gateway)
- dest = '{}%{}'.format(login_nick, room_suffix[1:])
- self.core.xmpp.send_message(
- mto=dest, mbody=login_command, mtype='chat')
-
- def completion_irc_login(self, the_input):
- """
- completion for /irc_login
- """
- args = the_input.text.split()
- if '' in args:
- args.remove('')
- pos = the_input.get_argument_position()
- sections = self.config.sections()
- if 'irc' in sections:
- sections.remove('irc')
- for section in args:
- try:
- sections.remove(section)
- except:
- pass
- return Completion(the_input.new_completion, sections, pos)
+ if not already_opened:
+ await self.join(gateway, section)
@command_args_parser.quoted(1, 1)
- def command_irc_join(self, args):
+ async def command_irc_join(self, args: Optional[List[str]]) -> None:
"""
/irc_join <room or server>
"""
if not args:
- return self.core.command.help('irc_join')
- sections = self.config.sections()
+ self.core.command.help('irc_join')
+ return
+ sections: List[str] = self.config.sections()
if 'irc' in sections:
sections.remove('irc')
- if args[0] in sections and self.config.get_by_tabname(
- 'rooms', args[0]):
- self.join_server_rooms(args[0])
+ if args[0] in sections:
+ try:
+ section_jid = JID(args[0])
+ except InvalidJID:
+ self.api.information(f'Invalid address: {args[0]}', 'Error')
+ return
+ #self.config.get_by_tabname('rooms', section_jid)
+ await self.join_server_rooms(section_jid)
else:
- self.join_room(args[0])
+ await self.join_room(args[0])
@command_args_parser.quoted(1, 1)
- def command_irc_query(self, args):
+ def command_irc_query(self, args: Optional[List[str]]) -> None:
"""
Open a private conversation with the given nickname, on the current IRC
server.
"""
if args is None:
- return self.core.command.help('irc_query')
+ self.core.command.help('irc_query')
+ return
current_tab_info = self.get_current_tab_irc_info()
if not current_tab_info:
return
@@ -336,14 +248,14 @@ class Plugin(BasePlugin):
else:
self.core.command.message('{}'.format(jid))
- def join_server_rooms(self, section):
+ async def join_server_rooms(self, section: JID) -> None:
"""
Join all the rooms configured for a section
(section = irc server)
"""
- gateway = self.config.get('gateway', 'irc.poez.io')
- rooms = self.config.get_by_tabname('rooms', section).split(':')
- nick = self.config.get_by_tabname('nickname', section)
+ gateway: str = self.config.getstr('gateway')
+ rooms: List[str] = self.config.get_by_tabname('rooms', section).split(':')
+ nick: str = self.config.get_by_tabname('nickname', section)
if nick:
nick = '/' + nick
else:
@@ -351,9 +263,9 @@ class Plugin(BasePlugin):
suffix = '%{}@{}{}'.format(section, gateway, nick)
for room in rooms:
- self.core.command.join(room + suffix)
+ await self.core.command.join(room + suffix)
- def join_room(self, name):
+ async def join_room(self, name: str) -> None:
"""
Join a room with only its name and the current tab
"""
@@ -361,22 +273,26 @@ class Plugin(BasePlugin):
if not current_tab_info:
return
server, gateway = current_tab_info
+ try:
+ server_jid = JID(server)
+ except InvalidJID:
+ return
room = '{}%{}@{}'.format(name, server, gateway)
- if self.config.get_by_tabname('nickname', server):
- room += '/' + self.config.get_by_tabname('nickname', server)
+ if self.config.get_by_tabname('nickname', server_jid.bare):
+ room += '/' + self.config.get_by_tabname('nickname', server_jid.bare)
- self.core.command.join(room)
+ await self.core.command.join(room)
- def get_current_tab_irc_info(self):
+ def get_current_tab_irc_info(self) -> Optional[Tuple[str, str]]:
"""
Return a tuple with the irc server and the gateway hostnames of the
current tab. If the current tab is not an IRC channel or private
conversation, a warning is displayed and None is returned
"""
- gateway = self.config.get('gateway', 'irc.poez.io')
+ gateway: str = self.config.getstr('gateway')
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')
@@ -397,11 +313,11 @@ class Plugin(BasePlugin):
return None
return server, gateway
- def completion_irc_join(self, the_input):
+ def completion_irc_join(self, the_input: Any) -> Completion:
"""
completion for /irc_join
"""
- sections = self.config.sections()
+ sections: List[str] = self.config.sections()
if 'irc' in sections:
sections.remove('irc')
return Completion(the_input.new_completion, sections, 1)
diff --git a/plugins/lastlog.py b/plugins/lastlog.py
new file mode 100644
index 00000000..1c48fa06
--- /dev/null
+++ b/plugins/lastlog.py
@@ -0,0 +1,61 @@
+#! /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 GPL-3.0+ license. See the COPYING file.
+
+"""
+ Search provided string in the buffer and return all results on the screen
+"""
+
+import re
+from typing import Optional
+from datetime import datetime
+
+from poezio.plugin import BasePlugin
+from poezio import tabs
+from poezio.text_buffer import TextBuffer
+from poezio.ui.types import Message as PMessage, InfoMessage
+
+
+def add_line(
+ text_buffer: TextBuffer,
+ text: str,
+ datetime: Optional[datetime] = None,
+ ) -> None:
+ """Adds a textual entry in the TextBuffer"""
+ text_buffer.add_message(InfoMessage(text, time=datetime))
+
+
+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 isinstance(message, PMessage) and \
+ search_re.search(message.txt) is not None:
+ res.append(message)
+ add_line(text_buffer, "%s> %s" % (message.nickname, message.txt), message.time)
+ 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..699215ea 100644
--- a/plugins/link.py
+++ b/plugins/link.py
@@ -76,7 +76,7 @@ Options
Set the default browser started by the plugin
.. _Unix FIFO: https://en.wikipedia.org/wiki/Named_pipe
-.. _daemon.py: http://dev.louiz.org/projects/poezio/repository/revisions/master/raw/poezio/daemon.py
+.. _daemon.py: https://lab.louiz.org/poezio/poezio/raw/main/poezio/daemon.py
"""
import platform
@@ -87,8 +87,17 @@ from poezio.xhtml import clean_text
from poezio import common
from poezio import tabs
-url_pattern = re.compile(r'\b(?:http[s]?://(?:\S+))|(?:magnet:\?(?:\S+))\b',
- re.I | re.U)
+url_pattern = re.compile(
+ r'\b'
+ '(?:http[s]?://(?:\S+))|'
+ '(?:magnet:\?(?:\S+))|'
+ '(?:aesgcm://(?:\S+))|'
+ '(?:gopher://(?:\S+))|'
+ '(?:gemini://(?:\S+))'
+ '\b',
+ re.I | re.U
+)
+
app_mapping = {
'Linux': 'xdg-open',
'Darwin': 'open',
@@ -97,7 +106,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..66ec8b70 100644
--- a/plugins/marquee.py
+++ b/plugins/marquee.py
@@ -34,6 +34,7 @@ Configuration
"""
+import asyncio
from poezio.plugin import BasePlugin
from poezio import tabs
from poezio import xhtml
@@ -41,7 +42,7 @@ from poezio.decorators import command_args_parser
def move(text, step, spacing):
- new_text = text + (" " * spacing)
+ new_text = text + ("\u00A0" * spacing)
return new_text[-(step % len(new_text)):] + new_text[:-(
step % len(new_text))]
@@ -56,19 +57,21 @@ 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')
@command_args_parser.raw
- def command_marquee(self, args):
+ async def command_marquee(self, args):
+ if not args:
+ return None
tab = self.api.current_tab()
args = xhtml.clean_text(xhtml.convert_simple_to_full_colors(args))
- tab.command_say(args)
+ await 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,
@@ -85,6 +88,6 @@ class Plugin(BasePlugin):
message.send()
event = self.api.create_delayed_event(
self.config.get("refresh"), self.delayed_event, jid, body,
- message["id"], step + 1, duration + self.config.get("refresh"),
+ msg_id, step + 1, duration + self.config.get("refresh"),
is_muctab)
self.api.add_timed_event(event)
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 9c80f390..6c15f3d2 100644
--- a/plugins/otr.py
+++ b/plugins/otr.py
@@ -184,7 +184,6 @@ and :term:`log` configuration parameters are tab-specific.
from gettext import gettext as _
import logging
-log = logging.getLogger(__name__)
import os
import html
import curses
@@ -194,10 +193,11 @@ import potr
from potr.context import NotEncryptedError, UnencryptedMessage, ErrorReceived, NotOTRMessage,\
STATE_ENCRYPTED, STATE_PLAINTEXT, STATE_FINISHED, Context, Account, crypt
+from slixmpp import JID, InvalidJID
+
from poezio import common
from poezio import xdg
from poezio import xhtml
-from poezio.common import safeJID
from poezio.config import config
from poezio.plugin import BasePlugin
from poezio.roster import roster
@@ -205,6 +205,9 @@ from poezio.tabs import StaticConversationTab, PrivateTab
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
+from poezio.ui.types import InfoMessage, Message
+
+log = logging.getLogger(__name__)
POLICY_FLAGS = {
'ALLOW_V1': False,
@@ -306,7 +309,7 @@ MESSAGE_INVALID = _('%(info)sThe message from %(jid_c)s%(jid)s%(info)s'
OTR_ERROR = _('%(info)sReceived the following error from '
'%(jid_c)s%(jid)s%(info)s:\x19o %(err)s')
-POTR_ERROR = _('%(info)sAn unspecified error in the OTR plugin occured:\n'
+POTR_ERROR = _('%(info)sAn unspecified error in the OTR plugin occurred:\n'
'%(exc)s')
TRUST_ADDED = _('%(info)sYou added %(jid_c)s%(bare_jid)s%(info)s with key '
@@ -325,7 +328,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):
@@ -344,7 +347,7 @@ class PoezioContext(Context):
self.xmpp = xmpp
self.core = core
self.flags = {}
- self.trustName = safeJID(peer).bare
+ self.trustName = JID(peer).bare
self.in_smp = False
self.smp_own = False
self.log = 0
@@ -374,7 +377,7 @@ class PoezioContext(Context):
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
'normal': '\x19%s}' % dump_tuple(get_theme().COLOR_NORMAL_TEXT),
'jid': self.peer,
- 'bare_jid': safeJID(self.peer).bare
+ 'bare_jid': JID(self.peer).bare
}
tab = self.core.tabs.by_name(self.peer)
@@ -385,25 +388,28 @@ class PoezioContext(Context):
log.debug('OTR conversation with %s refreshed', self.peer)
if self.getCurrentTrust():
msg = OTR_REFRESH_TRUSTED % format_dict
- tab.add_message(msg, typ=self.log)
+ tab.add_message(InfoMessage(msg))
else:
msg = OTR_REFRESH_UNTRUSTED % format_dict
- tab.add_message(msg, typ=self.log)
+ tab.add_message(InfoMessage(msg))
hl(tab)
elif newstate == STATE_FINISHED or newstate == STATE_PLAINTEXT:
log.debug('OTR conversation with %s finished', self.peer)
if tab:
- tab.add_message(OTR_END % format_dict, typ=self.log)
+ tab.add_message(InfoMessage(OTR_END % format_dict))
hl(tab)
elif newstate == STATE_ENCRYPTED and tab:
if self.getCurrentTrust():
- tab.add_message(OTR_START_TRUSTED % format_dict, typ=self.log)
+ tab.add_message(InfoMessage(OTR_START_TRUSTED % format_dict))
else:
format_dict['our_fpr'] = self.user.getPrivkey()
format_dict['remote_fpr'] = self.getCurrentKey()
- tab.add_message(OTR_TUTORIAL % format_dict, typ=0)
tab.add_message(
- OTR_START_UNTRUSTED % format_dict, typ=self.log)
+ InfoMessage(OTR_TUTORIAL % format_dict),
+ )
+ tab.add_message(
+ InfoMessage(OTR_START_UNTRUSTED % format_dict),
+ )
hl(tab)
log.debug('Set encryption state of %s to %s', self.peer,
@@ -455,8 +461,9 @@ class PoezioAccount(Account):
if acc != self.name or proto != 'xmpp':
continue
- jid = safeJID(ctx).bare
- if not jid:
+ try:
+ jid = JID(ctx).bare
+ except InvalidJID:
continue
self.setTrust(jid, fpr, trust)
except:
@@ -589,7 +596,7 @@ class Plugin(BasePlugin):
"""
Retrieve or create an OTR context
"""
- jid = safeJID(jid)
+ jid = JID(jid)
if jid.full not in self.contexts:
flags = POLICY_FLAGS.copy()
require = self.config.get_by_tabname(
@@ -607,6 +614,8 @@ class Plugin(BasePlugin):
"""
Message received
"""
+ if msg['from'].bare == self.core.xmpp.boundjid.bare:
+ return
format_dict = {
'jid_c': '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID),
'info': '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
@@ -639,7 +648,7 @@ class Plugin(BasePlugin):
# Received an OTR error
proto_error = err.args[0].error # pylint: disable=no-member
format_dict['err'] = proto_error.decode('utf-8', errors='replace')
- tab.add_message(OTR_ERROR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_ERROR % format_dict))
del msg['body']
del msg['html']
hl(tab)
@@ -649,7 +658,7 @@ class Plugin(BasePlugin):
# Encrypted message received, but unreadable as we do not have
# an OTR session in place.
text = MESSAGE_UNREADABLE % format_dict
- tab.add_message(text, jid=msg['from'], typ=0)
+ tab.add_message(InfoMessage(text))
hl(tab)
del msg['body']
del msg['html']
@@ -658,7 +667,7 @@ class Plugin(BasePlugin):
except crypt.InvalidParameterError:
# Malformed OTR payload and stuff
text = MESSAGE_INVALID % format_dict
- tab.add_message(text, jid=msg['from'], typ=0)
+ tab.add_message(InfoMessage(text))
hl(tab)
del msg['body']
del msg['html']
@@ -669,7 +678,7 @@ class Plugin(BasePlugin):
import traceback
exc = traceback.format_exc()
format_dict['exc'] = exc
- tab.add_message(POTR_ERROR % format_dict, typ=0)
+ tab.add_message(InfoMessage(POTR_ERROR % format_dict))
log.error('Unspecified error in the OTR plugin', exc_info=True)
return
# No error, proceed with the message
@@ -688,10 +697,10 @@ class Plugin(BasePlugin):
abort = get_tlv(tlvs, potr.proto.SMPABORTTLV)
if abort:
ctx.reset_smp()
- tab.add_message(SMP_ABORTED_PEER % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED_PEER % format_dict))
elif ctx.in_smp and not ctx.smpIsValid():
ctx.reset_smp()
- tab.add_message(SMP_ABORTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED % format_dict))
elif smp1 or smp1q:
# Received an SMP request (with a question or not)
if smp1q:
@@ -709,22 +718,22 @@ class Plugin(BasePlugin):
# we did not initiate it
ctx.smp_own = False
format_dict['q'] = question
- tab.add_message(SMP_REQUESTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_REQUESTED % format_dict))
elif smp2:
# SMP reply received
if not ctx.in_smp:
ctx.reset_smp()
else:
- tab.add_message(SMP_PROGRESS % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_PROGRESS % format_dict))
elif smp3 or smp4:
# Type 4 (SMP message 3) or 5 (SMP message 4) TLVs received
# in both cases it is the final message of the SMP exchange
if ctx.smpIsSuccess():
- tab.add_message(SMP_SUCCESS % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_SUCCESS % format_dict))
if not ctx.getCurrentTrust():
- tab.add_message(SMP_RECIPROCATE % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_RECIPROCATE % format_dict))
else:
- tab.add_message(SMP_FAIL % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_FAIL % format_dict))
ctx.reset_smp()
hl(tab)
self.core.refresh_window()
@@ -736,7 +745,13 @@ class Plugin(BasePlugin):
"""
format_dict['msg'] = err.args[0].decode('utf-8')
text = MESSAGE_UNENCRYPTED % format_dict
- tab.add_message(text, jid=msg['from'], typ=ctx.log)
+ tab.add_message(
+ Message(
+ text,
+ nickname=tab.nick,
+ jid=msg['from'],
+ ),
+ )
del msg['body']
del msg['html']
hl(tab)
@@ -780,12 +795,14 @@ class Plugin(BasePlugin):
if decode_newlines:
body = body.replace('<br/>', '\n').replace('<br>', '\n')
tab.add_message(
- body,
- nickname=tab.nick,
- jid=msg['from'],
- forced_user=user,
- typ=ctx.log,
- nick_color=nick_color)
+ Message(
+ body,
+ nickname=tab.nick,
+ jid=msg['from'],
+ user=user,
+ nick_color=nick_color
+ ),
+ )
hl(tab)
self.core.refresh_window()
del msg['body']
@@ -795,9 +812,11 @@ class Plugin(BasePlugin):
Find an OTR session from a bare JID.
"""
for ctx in self.contexts:
- if safeJID(
- ctx
- ).bare == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED:
+ try:
+ jid = JID(ctx).bare
+ except InvalidJID:
+ continue
+ if jid == bare_jid and self.contexts[ctx].state == STATE_ENCRYPTED:
return self.contexts[ctx]
return None
@@ -806,7 +825,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),
@@ -826,19 +845,21 @@ class Plugin(BasePlugin):
tab.send_chat_state('inactive', always_send=True)
tab.add_message(
- msg['body'],
- nickname=self.core.own_nick or tab.own_nick,
- nick_color=get_theme().COLOR_OWN_NICK,
- identifier=msg['id'],
- jid=self.core.xmpp.boundjid,
- typ=ctx.log)
+ Message(
+ msg['body'],
+ nickname=self.core.own_nick or tab.own_nick,
+ nick_color=get_theme().COLOR_OWN_NICK,
+ identifier=msg['id'],
+ jid=self.core.xmpp.boundjid,
+ ),
+ )
# remove everything from the message so that it doesn’t get sent
del msg['body']
del msg['replace']
del msg['html']
elif is_relevant(tab) and ctx and ctx.getPolicy('REQUIRE_ENCRYPTION'):
warning_msg = MESSAGE_NOT_SENT % format_dict
- tab.add_message(warning_msg, typ=0)
+ tab.add_message(InfoMessage(warning_msg))
del msg['body']
del msg['replace']
del msg['html']
@@ -846,7 +867,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]
@@ -856,7 +877,7 @@ class Plugin(BasePlugin):
('\n - /message %s' % jid) for jid in res)
format_dict['help'] = help_msg
warning_msg = INCOMPATIBLE_TAB % format_dict
- tab.add_message(warning_msg, typ=0)
+ tab.add_message(InfoMessage(warning_msg))
del msg['body']
del msg['replace']
del msg['html']
@@ -866,7 +887,11 @@ class Plugin(BasePlugin):
Returns the text to display in the infobar (the OTR status)
"""
context = self.get_context(jid)
- if safeJID(jid).bare == jid and context.state != STATE_ENCRYPTED:
+ try:
+ bare_jid = JID(jid).bare
+ except InvalidJID:
+ bare_jid = ''
+ if bare_jid == jid and context.state != STATE_ENCRYPTED:
ctx = self.find_encrypted_context_with_matching(jid)
if ctx:
context = ctx
@@ -884,13 +909,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
@@ -900,22 +925,22 @@ class Plugin(BasePlugin):
self.otr_start(tab, name, format_dict)
elif action == 'ourfpr':
format_dict['fpr'] = self.account.getPrivkey()
- tab.add_message(OTR_OWN_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_OWN_FPR % format_dict))
elif action == 'fpr':
if name in self.contexts:
ctx = self.contexts[name]
if ctx.getCurrentKey() is not None:
format_dict['fpr'] = ctx.getCurrentKey()
- tab.add_message(OTR_REMOTE_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_REMOTE_FPR % format_dict))
else:
- tab.add_message(OTR_NO_FPR % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_NO_FPR % format_dict))
elif action == 'drop':
# drop the privkey (and obviously, end the current conversations before that)
for context in self.contexts.values():
if context.state not in (STATE_FINISHED, STATE_PLAINTEXT):
context.disconnect()
self.account.drop_privkey()
- tab.add_message(KEY_DROPPED % format_dict, typ=0)
+ tab.add_message(InfoMessage(KEY_DROPPED % format_dict))
elif action == 'trust':
ctx = self.get_context(name)
key = ctx.getCurrentKey()
@@ -927,7 +952,7 @@ class Plugin(BasePlugin):
format_dict['key'] = key
ctx.setTrust(fpr, 'verified')
self.account.saveTrusts()
- tab.add_message(TRUST_ADDED % format_dict, typ=0)
+ tab.add_message(InfoMessage(TRUST_ADDED % format_dict))
elif action == 'untrust':
ctx = self.get_context(name)
key = ctx.getCurrentKey()
@@ -939,7 +964,7 @@ class Plugin(BasePlugin):
format_dict['key'] = key
ctx.setTrust(fpr, '')
self.account.saveTrusts()
- tab.add_message(TRUST_REMOVED % format_dict, typ=0)
+ tab.add_message(InfoMessage(TRUST_REMOVED % format_dict))
self.core.refresh_window()
def otr_start(self, tab, name, format_dict):
@@ -954,7 +979,7 @@ class Plugin(BasePlugin):
if otr.state != STATE_ENCRYPTED:
format_dict['secs'] = secs
text = OTR_NOT_ENABLED % format_dict
- tab.add_message(text, typ=0)
+ tab.add_message(InfoMessage(text))
self.core.refresh_window()
if secs > 0:
@@ -962,7 +987,7 @@ class Plugin(BasePlugin):
self.api.add_timed_event(event)
body = self.get_context(name).sendMessage(0, b'?OTRv?').decode()
self.core.xmpp.send_message(mto=name, mtype='chat', mbody=body)
- tab.add_message(OTR_REQUEST % format_dict, typ=0)
+ tab.add_message(InfoMessage(OTR_REQUEST % format_dict))
@staticmethod
def completion_otr(the_input):
@@ -991,12 +1016,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)
@@ -1012,13 +1037,13 @@ class Plugin(BasePlugin):
ctx.smpInit(secret, question)
else:
ctx.smpInit(secret)
- tab.add_message(SMP_INITIATED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_INITIATED % format_dict))
elif action == 'answer':
ctx.smpGotSecret(secret)
elif action == 'abort':
if ctx.in_smp:
ctx.smpAbort()
- tab.add_message(SMP_ABORTED % format_dict, typ=0)
+ tab.add_message(InfoMessage(SMP_ABORTED % format_dict))
self.core.refresh_window()
@staticmethod
diff --git a/plugins/ping.py b/plugins/ping.py
index 4868ccf9..cc987bf0 100644
--- a/plugins/ping.py
+++ b/plugins/ping.py
@@ -21,11 +21,13 @@ Command
In a private or a direct conversation, you can do ``/ping`` to ping
the current interlocutor.
"""
+import asyncio
+from slixmpp import InvalidJID, JID
+from slixmpp.exceptions import IqTimeout
from poezio.decorators import command_args_parser
from poezio.plugin import BasePlugin
from poezio.roster import roster
-from poezio.common import safeJID
from poezio.contact import Contact, Resource
from poezio.core.structs import Completion
from poezio import tabs
@@ -57,7 +59,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',
@@ -69,13 +71,19 @@ class Plugin(BasePlugin):
completion=self.completion_ping)
@command_args_parser.raw
- def command_ping(self, arg):
+ async def command_ping(self, arg):
if not arg:
return self.core.command.help('ping')
- jid = safeJID(arg)
+ try:
+ jid = JID(arg)
+ except InvalidJID:
+ return self.api.information('Invalid JID: %s' % arg, 'Error')
start = time.time()
- def callback(iq):
+ try:
+ iq = await self.core.xmpp.plugin['xep_0199'].send_ping(
+ jid=jid, timeout=10
+ )
delay = time.time() - start
error = False
reply = ''
@@ -98,13 +106,11 @@ class Plugin(BasePlugin):
message = '%s responded to ping after %ss%s' % (
jid, round(delay, 4), reply)
self.api.information(message, 'Info')
-
- def timeout(iq):
+ except IqTimeout:
self.api.information(
- '%s did not respond to ping after 10s: timeout' % jid, 'Info')
-
- self.core.xmpp.plugin['xep_0199'].send_ping(
- jid=jid, callback=callback, timeout=10, timeout_callback=timeout)
+ '%s did not respond to ping after 10s: timeout' % jid,
+ 'Info'
+ )
def completion_muc_ping(self, the_input):
users = [user.nick for user in self.api.current_tab().users]
@@ -114,9 +120,12 @@ class Plugin(BasePlugin):
@command_args_parser.raw
def command_private_ping(self, arg):
- if arg:
- return self.command_ping(arg)
- self.command_ping(self.api.current_tab().name)
+ jid = arg
+ if not arg:
+ jid = self.api.current_tab().jid
+ asyncio.create_task(
+ self.command_ping(jid)
+ )
@command_args_parser.raw
def command_muc_ping(self, arg):
@@ -124,24 +133,32 @@ 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)
- self.command_ping(jid.full)
+ try:
+ jid = JID(arg)
+ except InvalidJID:
+ return self.api.information('Invalid JID: %s' % arg, 'Error')
+ asyncio.create_task(
+ self.command_ping(jid.full)
+ )
@command_args_parser.raw
def command_roster_ping(self, arg):
if arg:
- self.command_ping(arg)
+ jid = arg
else:
current = self.api.current_tab().selected_row
if isinstance(current, Resource):
- self.command_ping(current.jid)
+ jid = current.jid
elif isinstance(current, Contact):
res = current.get_highest_priority_resource()
if res is not None:
- self.command_ping(res.jid)
+ jid =res.jid
+ asyncio.create_task(
+ self.command_ping(jid)
+ )
def resources(self):
l = []
diff --git a/plugins/qr.py b/plugins/qr.py
new file mode 100755
index 00000000..735c3002
--- /dev/null
+++ b/plugins/qr.py
@@ -0,0 +1,184 @@
+#!/usr/bin/env python3
+
+import io
+import logging
+import qrcode
+
+from typing import Dict, Callable
+
+from slixmpp import JID, InvalidJID
+
+from poezio import windows
+from poezio.tabs import Tab
+from poezio.core.structs import Command
+from poezio.decorators import command_args_parser
+from poezio.plugin import BasePlugin
+from poezio.theming import get_theme, to_curses_attr
+from poezio.windows.base_wins import Win
+
+log = logging.getLogger(__name__)
+
+class QrWindow(Win):
+ __slots__ = ('qr', 'invert', 'inverted')
+
+ str_invert = " Invert "
+ str_close = " Close "
+
+ def __init__(self, qr: str) -> None:
+ self.qr = qr
+ self.invert = True
+ self.inverted = True
+
+ def refresh(self) -> None:
+ self._win.erase()
+ # draw QR code
+ code = qrcode.QRCode()
+ code.add_data(self.qr)
+ out = io.StringIO()
+ code.print_ascii(out, invert=self.inverted)
+ self.addstr(" " + self.qr + "\n")
+ self.addstr(out.getvalue(), to_curses_attr((15, 0)))
+ self.addstr(" ")
+
+ col = to_curses_attr(get_theme().COLOR_TAB_NORMAL)
+
+ if self.invert:
+ self.addstr(self.str_invert, col)
+ else:
+ self.addstr(self.str_invert)
+
+ self.addstr(" ")
+
+ if self.invert:
+ self.addstr(self.str_close)
+ else:
+ self.addstr(self.str_close, col)
+
+ self._refresh()
+
+ def toggle_choice(self) -> None:
+ self.invert = not self.invert
+
+ def engage(self) -> bool:
+ if self.invert:
+ self.inverted = not self.inverted
+ return False
+ else:
+ return True
+
+class QrTab(Tab):
+ plugin_commands = {} # type: Dict[str, Command]
+ plugin_keys = {} # type: Dict[str, Callable]
+
+ def __init__(self, core, qr):
+ Tab.__init__(self, core)
+ self.state = 'highlight'
+ self.text = qr
+ self._name = qr
+ self.topic_win = windows.Topic()
+ self.topic_win.set_message(qr)
+ self.qr_win = QrWindow(qr)
+ self.help_win = windows.HelpText(
+ "Choose with arrow keys and press enter")
+ self.key_func['^I'] = self.toggle_choice
+ self.key_func[' '] = self.toggle_choice
+ self.key_func['KEY_LEFT'] = self.toggle_choice
+ self.key_func['KEY_RIGHT'] = self.toggle_choice
+ self.key_func['^M'] = self.engage
+ self.resize()
+ self.update_commands()
+ self.update_keys()
+
+ def resize(self):
+ self.need_resize = False
+ self.topic_win.resize(1, self.width, 0, 0)
+ self.qr_win.resize(self.height-3, self.width, 1, 0)
+ self.help_win.resize(1, self.width, self.height-1, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ log.debug(' TAB Refresh: %s', self.__class__.__name__)
+ self.refresh_tab_win()
+ self.info_win.refresh()
+ self.topic_win.refresh()
+ self.qr_win.refresh()
+ self.help_win.refresh()
+
+ def on_input(self, key, raw):
+ if not raw and key in self.key_func:
+ return self.key_func[key]()
+
+ def toggle_choice(self):
+ log.debug(' TAB toggle_choice: %s', self.__class__.__name__)
+ self.qr_win.toggle_choice()
+ self.refresh()
+ self.core.doupdate()
+
+ def engage(self):
+ log.debug(' TAB engage: %s', self.__class__.__name__)
+ if self.qr_win.engage():
+ self.core.close_tab(self)
+ else:
+ self.refresh()
+ self.core.doupdate()
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.api.add_command(
+ 'qr',
+ self.command_qr,
+ usage='<message>',
+ short='Display a QR code',
+ help='Display a QR code of <message> in a new tab')
+ self.api.add_command(
+ 'invitation',
+ self.command_invite,
+ usage='[<server>]',
+ short='Invite a user',
+ help='Generate a XEP-0401 invitation on your server or on <server> and display a QR code')
+
+ def command_qr(self, msg):
+ t = QrTab(self.core, msg)
+ self.core.add_tab(t, True)
+ self.core.doupdate()
+
+ def on_next(self, iq, adhoc_session):
+ status = iq['command']['status']
+ xform = iq.xml.find(
+ '{http://jabber.org/protocol/commands}command/{jabber:x:data}x')
+ if xform is not None:
+ form = self.core.xmpp.plugin['xep_0004'].build_form(xform)
+ else:
+ form = None
+ uri = None
+ if status == 'completed' and form:
+ for field in form:
+ log.debug(' field: %s -> %s', field['var'], field['value'])
+ if field['var'] == 'landing-url' and field['value']:
+ uri = field.get_value(convert=False)
+ if field['var'] == 'uri' and field['value'] and uri is None:
+ uri = field.get_value(convert=False)
+ if uri:
+ t = QrTab(self.core, uri)
+ self.core.add_tab(t, True)
+ self.core.doupdate()
+ else:
+ self.core.handler.next_adhoc_step(iq, adhoc_session)
+
+
+ @command_args_parser.quoted(0, 1, defaults=[])
+ def command_invite(self, args):
+ server = self.core.xmpp.boundjid.domain
+ if len(args) > 0:
+ try:
+ server = JID(args[0])
+ except InvalidJID:
+ self.api.information(f'Invalid JID: {args[0]}', 'Error')
+ return
+ session = {
+ 'next' : self.on_next,
+ 'error': self.core.handler.adhoc_error
+ }
+ self.core.xmpp.plugin['xep_0050'].start_command(server, 'urn:xmpp:invite#invite', session)
+
diff --git a/plugins/quote.py b/plugins/quote.py
index b412cd9a..d7bc1e2a 100644
--- a/plugins/quote.py
+++ b/plugins/quote.py
@@ -45,8 +45,10 @@ Options
"""
from poezio.core.structs import Completion
+from poezio.ui.types import Message
from poezio.plugin import BasePlugin
from poezio.xhtml import clean_text
+from poezio.theming import get_theme
from poezio import common
from poezio import tabs
@@ -56,7 +58,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',
@@ -74,13 +76,14 @@ class Plugin(BasePlugin):
return self.api.run_command('/help quote')
message = self.find_message(message)
if message:
+ str_time = message.time.strftime(get_theme().SHORT_TIME_FORMAT)
before = self.config.get('before_quote', '') % {
'nick': message.nickname or '',
- 'time': message.str_time
+ 'time': str_time,
}
after = self.config.get('after_quote', '') % {
'nick': message.nickname or '',
- 'time': message.str_time
+ 'time': str_time,
}
self.core.insert_input_text(
'%(before)s%(quote)s%(after)s' % {
@@ -96,7 +99,7 @@ class Plugin(BasePlugin):
if not messages:
return None
for message in messages[::-1]:
- if clean_text(message.txt) == txt:
+ if isinstance(message, Message) and clean_text(message.txt) == txt:
return message
return None
@@ -114,5 +117,8 @@ class Plugin(BasePlugin):
messages = list(filter(message_match, messages))
elif len(args) > 1:
return False
- return Completion(the_input.auto_completion,
- [clean_text(msg.txt) for msg in messages[::-1]], '')
+ return Completion(
+ the_input.auto_completion,
+ [clean_text(msg.txt) for msg in messages[::-1] if isinstance(msg, Message)],
+ ''
+ )
diff --git a/plugins/rainbow.py b/plugins/rainbow.py
index 4ab0b9ac..e5987089 100644
--- a/plugins/rainbow.py
+++ b/plugins/rainbow.py
@@ -14,7 +14,7 @@ Usage
.. note:: Can create fun things when used with :ref:`The figlet plugin <figlet-plugin>`.
-.. _#3273: https://dev.louiz.org/issues/3273
+.. _#3273: https://lab.louiz.org/poezio/poezio/-/issues/3273
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
diff --git a/plugins/remove_get_trackers.py b/plugins/remove_get_trackers.py
new file mode 100644
index 00000000..db1e87f3
--- /dev/null
+++ b/plugins/remove_get_trackers.py
@@ -0,0 +1,24 @@
+"""
+Remove GET trackers from URLs in sent messages.
+"""
+from poezio.plugin import BasePlugin
+import re
+
+class Plugin(BasePlugin):
+ def init(self):
+ self.api.information('This plugin is deprecated and will be replaced by \'untrackme\'.', 'Warning')
+
+ self.api.add_event_handler('muc_say', self.remove_get_trackers)
+ self.api.add_event_handler('conversation_say', self.remove_get_trackers)
+ self.api.add_event_handler('private_say', self.remove_get_trackers)
+
+ def remove_get_trackers(self, msg, tab):
+ # fbclid: used globally (Facebook)
+ # utm_*: used globally https://en.wikipedia.org/wiki/UTM_parameters
+ # ncid: DoubleClick (Google)
+ # ref_src, ref_url: twitter
+ # Others exist but are excluded because they are not common.
+ # See https://en.wikipedia.org/wiki/UTM_parameters
+ msg['body'] = re.sub('(https?://[^ ]+)&?(fbclid|dclid|ncid|utm_source|utm_medium|utm_campaign|utm_term|utm_content|ref_src|ref_url)=[^ &#]*',
+ r'\1',
+ msg['body'])
diff --git a/plugins/reorder.py b/plugins/reorder.py
index 7308196d..158b89bb 100644
--- a/plugins/reorder.py
+++ b/plugins/reorder.py
@@ -59,6 +59,8 @@ And finally, the ``[tab name]`` must be:
- For a type ``static``, the full JID of the contact
"""
+from slixmpp import InvalidJID, JID
+
from poezio import tabs
from poezio.decorators import command_args_parser
from poezio.plugin import BasePlugin
@@ -90,7 +92,11 @@ def parse_config(tab_config):
if pos in result or pos <= 0:
return None
- typ, name = tab_config.get(option, default=':').split(':', maxsplit=1)
+ spec = tab_config.get(option, default=':').split(':', maxsplit=1)
+ # Gap tabs are recreated automatically if there's a gap in indices.
+ if spec == 'empty':
+ return None
+ typ, name = spec
if typ not in TEXT_TO_TAB:
return None
result[pos] = (TEXT_TO_TAB[typ], name)
@@ -111,12 +117,15 @@ def parse_runtime_tablist(tablist):
for tab in tablist[1:]:
i += 1
result = check_tab(tab)
- if result:
- props.append((i, '%s:%s' % (result, tab.name)))
+ # Don't serialize gap tabs as they're recreated automatically
+ if result != 'empty' and isinstance(tab, tuple(TEXT_TO_TAB.values())):
+ props.append((i, '%s:%s' % (result, tab.jid.full)))
return props
class Plugin(BasePlugin):
+ """reorder plugin"""
+
def init(self):
self.api.add_command(
'reorder',
@@ -129,20 +138,24 @@ class Plugin(BasePlugin):
help='Save the current tab layout')
@command_args_parser.ignored
- def command_save_order(self):
+ def command_save_order(self) -> None:
+ """
+ /save_order
+ """
conf = parse_runtime_tablist(self.core.tabs)
for key, value in conf:
self.config.set(key, value)
self.api.information('Tab order saved', 'Info')
@command_args_parser.ignored
- def command_reorder(self):
+ def command_reorder(self) -> None:
"""
/reorder
"""
tabs_spec = parse_config(self.config)
if not tabs_spec:
- return self.api.information('Invalid reorder config', 'Error')
+ self.api.information('Invalid reorder config', 'Error')
+ return None
old_tabs = self.core.tabs.get_tabs()
roster = old_tabs.pop(0)
@@ -154,22 +167,37 @@ class Plugin(BasePlugin):
for pos in sorted(tabs_spec):
if create_gaps and pos > last + 1:
new_tabs += [
- tabs.GapTab(self.core) for i in range(pos - last - 1)
+ tabs.GapTab() for i in range(pos - last - 1)
]
- cls, name = tabs_spec[pos]
- tab = self.core.tabs.by_name_and_class(name, cls=cls)
- if tab and tab in old_tabs:
- new_tabs.append(tab)
- old_tabs.remove(tab)
- else:
- self.api.information('Tab %s not found' % name, 'Warning')
+ cls, jid = tabs_spec[pos]
+ try:
+ jid = JID(jid)
+ tab = self.core.tabs.by_name_and_class(str(jid), cls=cls)
+ if tab and tab in old_tabs:
+ new_tabs.append(tab)
+ old_tabs.remove(tab)
+ else:
+ # TODO: Add support for MucTab. Requires nickname.
+ if cls in (tabs.DynamicConversationTab, tabs.StaticConversationTab):
+ self.api.information('Tab %s not found. Creating it' % jid, 'Warning')
+ new_tab = cls(self.core, jid)
+ new_tabs.append(new_tab)
+ else:
+ new_tabs.append(tabs.GapTab())
+ except:
+ self.api.information('Failed to create tab \'%s\'.' % jid, 'Error')
if create_gaps:
- new_tabs.append(tabs.GapTab(self.core))
- last = pos
+ new_tabs.append(tabs.GapTab())
+ finally:
+ last = pos
for tab in old_tabs:
if tab:
new_tabs.append(tab)
+ # TODO: Ensure we don't break poezio and call this with whatever
+ # tablist we have. The roster tab at least needs to be in there.
self.core.tabs.replace_tabs(new_tabs)
self.core.refresh_window()
+
+ return None
diff --git a/plugins/replace.py b/plugins/replace.py
index 3202721c..02059a18 100644
--- a/plugins/replace.py
+++ b/plugins/replace.py
@@ -23,7 +23,7 @@ Add your own pattern
--------------------
You can easily edit this plugin to add your own patterns. For example if
-don’t want to search for an insult everytime you’re angry, you can create a
+don’t want to search for an insult every time you’re angry, you can create a
curse pattern this way:
- In the init(self) method of the Plugin class, add something like
@@ -91,7 +91,7 @@ def replace_time(message, tab):
def replace_date(message, tab):
- return datetime.datetime.now().strftime("%x")
+ return datetime.datetime.now().strftime("%Y-%m-%d")
def replace_datetime(message, tab):
@@ -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/screen_detach.py b/plugins/screen_detach.py
index 0a2514c4..1f908513 100644
--- a/plugins/screen_detach.py
+++ b/plugins/screen_detach.py
@@ -43,10 +43,10 @@ DEFAULT_CONFIG = {
# overload if this is not how your stuff
# is configured
try:
- LOGIN = os.getlogin()
+ LOGIN = os.getlogin() or ''
LOGIN_TMUX = os.getuid()
except Exception:
- LOGIN = os.getenv('USER')
+ LOGIN = os.getenv('USER') or ''
LOGIN_TMUX = os.getuid()
SCREEN_DIR = '/var/run/screens/S-%s' % LOGIN
diff --git a/plugins/send_delayed.py b/plugins/send_delayed.py
index 846fccd1..92ed97c1 100644
--- a/plugins/send_delayed.py
+++ b/plugins/send_delayed.py
@@ -18,6 +18,7 @@ This plugin adds a command to the chat tabs.
"""
+import asyncio
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
@@ -28,7 +29,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',
@@ -74,6 +75,6 @@ class Plugin(BasePlugin):
tab = args[0]
# anything could happen to the tab during the interval
try:
- tab.command_say(args[1])
+ asyncio.ensure_future(tab.command_say(args[1]))
except:
pass
diff --git a/plugins/server_part.py b/plugins/server_part.py
index 7a71d94b..cae2248e 100644
--- a/plugins/server_part.py
+++ b/plugins/server_part.py
@@ -16,10 +16,10 @@ Command
"""
+from slixmpp import JID, InvalidJID
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
from poezio.decorators import command_args_parser
-from poezio.common import safeJID
from poezio.core.structs import Completion
@@ -39,16 +39,18 @@ 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
- if not jid:
+ try:
+ jid = JID(args[0]).domain
+ except InvalidJID:
return self.core.command_help('server_part')
message = None
else:
- jid = safeJID(args[0]).domain
- if not jid:
+ try:
+ jid = JID(args[0]).domain
+ except InvalidJID:
return self.core.command_help('server_part')
message = args[1]
@@ -60,6 +62,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/simple_notify.py b/plugins/simple_notify.py
index cfb65e9b..29418f40 100644
--- a/plugins/simple_notify.py
+++ b/plugins/simple_notify.py
@@ -114,10 +114,11 @@ class Plugin(BasePlugin):
def on_conversation_msg(self, message, tab):
fro = message['from'].bare
- self.do_notify(message, fro)
+ if fro.bare != self.core.xmpp.boundjid.bare:
+ self.do_notify(message, fro)
def on_muc_msg(self, message, tab):
- # Dont notify if message is from yourself
+ # Don't notify if message is from yourself
if message['from'].resource == tab.own_nick:
return
diff --git a/plugins/sticker.py b/plugins/sticker.py
new file mode 100644
index 00000000..c9deacc0
--- /dev/null
+++ b/plugins/sticker.py
@@ -0,0 +1,97 @@
+'''
+This plugin lets the user select and send a sticker from a pack of stickers.
+
+The protocol used here is based on XEP-0363 and XEP-0066, while a future
+version may use XEP-0449 instead.
+
+Command
+-------
+
+.. glossary::
+ /sticker
+ **Usage:** ``/sticker <pack>``
+
+ Opens a picker tool, and send the sticker which has been selected.
+
+Configuration options
+---------------------
+
+.. glossary::
+ sticker_picker
+ **Default:** ``poezio-sticker-picker``
+
+ The command to invoke as a sticker picker. A sample one is provided in
+ tools/sticker-picker.
+
+ stickers_dir
+ **Default:** ``XDG_DATA_HOME/poezio/stickers``
+
+ The directory under which the sticker packs can be found.
+'''
+
+import asyncio
+import concurrent.futures
+from poezio import xdg
+from poezio.plugin import BasePlugin
+from poezio.config import config
+from poezio.decorators import command_args_parser
+from poezio.core.structs import Completion
+from pathlib import Path
+from asyncio.subprocess import PIPE, DEVNULL
+
+class Plugin(BasePlugin):
+ dependencies = {'upload'}
+
+ def init(self):
+ # The command to use as a picker helper.
+ self.picker_command = config.getstr('sticker_picker') or 'poezio-sticker-picker'
+
+ # Select and create the stickers directory.
+ directory = config.getstr('stickers_dir')
+ if directory:
+ self.directory = Path(directory).expanduser()
+ else:
+ self.directory = xdg.DATA_HOME / 'stickers'
+ self.directory.mkdir(parents=True, exist_ok=True)
+
+ self.upload = self.refs['upload']
+ self.api.add_command('sticker', self.command_sticker,
+ usage='<sticker pack>',
+ short='Send a sticker',
+ help='Send a sticker, with a helper GUI sticker picker',
+ completion=self.completion_sticker)
+
+ def command_sticker(self, pack):
+ '''
+ Sends a sticker
+ '''
+ if not pack:
+ self.api.information('Missing sticker pack argument.', 'Error')
+ return
+ async def run_command(tab, path: Path):
+ try:
+ process = await asyncio.create_subprocess_exec(
+ self.picker_command, path, stdout=PIPE, stderr=PIPE)
+ sticker, stderr = await process.communicate()
+ except FileNotFoundError as err:
+ self.api.information('Failed to launch the sticker picker: %s' % err, 'Error')
+ return
+ else:
+ if process.returncode != 0:
+ self.api.information('Sticker picker failed: %s' % stderr.decode(), 'Error')
+ return
+ if sticker:
+ filename = sticker.decode().rstrip()
+ self.api.information('Sending sticker %s' % filename, 'Info')
+ await self.upload.send_upload(path / filename, tab)
+ tab = self.api.current_tab()
+ path = self.directory / pack
+ asyncio.create_task(run_command(tab, path))
+
+ def completion_sticker(self, the_input):
+ '''
+ Completion for /sticker
+ '''
+ txt = the_input.get_text()[9:]
+ directories = [directory.name for directory in self.directory.glob(txt + '*')]
+ return Completion(the_input.auto_completion, directories, quotify=False)
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..cd72a9e5 100644
--- a/plugins/tell.py
+++ b/plugins/tell.py
@@ -25,6 +25,7 @@ This plugin defines two new commands for chatroom tabs:
List all queued messages for the current chatroom.
"""
+import asyncio
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
@@ -66,7 +67,7 @@ class Plugin(BasePlugin):
if nick not in self.tabs[tab]:
return
for i in self.tabs[tab][nick]:
- tab.command_say("%s: %s" % (nick, i))
+ asyncio.ensure_future(tab.command_say("%s: %s" % (nick, i)))
del self.tabs[tab][nick]
@command_args_parser.ignored
@@ -75,7 +76,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..6ce511a0 100644
--- a/plugins/time_marker.py
+++ b/plugins/time_marker.py
@@ -31,12 +31,13 @@ Messages like “2 hours, 25 minutes passed…” are automatically displayed in
from poezio.plugin import BasePlugin
from datetime import datetime, timedelta
+from poezio.ui.types import InfoMessage
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,10 +67,11 @@ 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)):
tab.add_message(
- "%s passed…" % (format_timedelta(delta), ), str_time='')
+ InfoMessage("%s passed…" % (format_timedelta(delta), ))
+ )
diff --git a/plugins/untrackme.py b/plugins/untrackme.py
new file mode 100644
index 00000000..ceddc5c5
--- /dev/null
+++ b/plugins/untrackme.py
@@ -0,0 +1,140 @@
+"""
+ UntrackMe wannabe plugin
+"""
+
+from typing import Callable, Dict, List, Tuple, Union
+
+import re
+import logging
+from slixmpp import Message
+from poezio import tabs
+from poezio.plugin import BasePlugin
+from urllib.parse import quote as urlquote
+
+
+log = logging.getLogger(__name__)
+
+ChatTabs = Union[
+ tabs.MucTab,
+ tabs.DynamicConversationTab,
+ tabs.StaticConversationTab,
+ tabs.PrivateTab,
+]
+
+RE_URL: re.Pattern = re.compile('https?://(?P<host>[^/]+)(?P<rest>[^ ]*)')
+
+SERVICES: Dict[str, Tuple[str, bool]] = { # host: (service, proxy)
+ 'm.youtube.com': ('invidious', False),
+ 'www.youtube.com': ('invidious', False),
+ 'youtube.com': ('invidious', False),
+ 'youtu.be': ('invidious', False),
+ 'youtube-nocookie.com': ('invidious', False),
+ 'mobile.twitter.com': ('nitter', False),
+ 'www.twitter.com': ('nitter', False),
+ 'twitter.com': ('nitter', False),
+ 'pic.twitter.com': ('nitter_img', True),
+ 'pbs.twimg.com': ('nitter_img', True),
+ 'instagram.com': ('bibliogram', False),
+ 'www.instagram.com': ('bibliogram', False),
+ 'm.instagram.com': ('bibliogram', False),
+}
+
+def proxy(service: str) -> Callable[[str], str]:
+ """Some services require the original url"""
+ def inner(origin: str) -> str:
+ return service + urlquote(origin)
+ return inner
+
+
+class Plugin(BasePlugin):
+ """UntrackMe"""
+
+ default_config: Dict[str, Dict[str, Union[str, bool]]] = {
+ 'default': {
+ 'cleanup': True,
+ 'redirect': True,
+ 'display_corrections': False,
+ },
+ 'services': {
+ 'invidious': 'https://invidious.snopyta.org',
+ 'nitter': 'https://nitter.net',
+ 'bibliogram': 'https://bibliogram.art',
+ },
+ }
+
+ def init(self):
+ nitter_img = self.config.get('nitter', section='services') + '/pic/'
+ self.config.set('nitter_img', nitter_img, section='services')
+
+ self.api.add_event_handler('muc_say', self.handle_msg)
+ self.api.add_event_handler('conversation_say', self.handle_msg)
+ self.api.add_event_handler('private_say', self.handle_msg)
+
+ self.api.add_event_handler('muc_msg', self.handle_msg)
+ self.api.add_event_handler('conversation_msg', self.handle_msg)
+ self.api.add_event_handler('private_msg', self.handle_msg)
+
+ def map_services(self, match: re.Match) -> str:
+ """
+ If it matches a host that we know about, change the domain for the
+ alternative service. Some hosts needs to be proxied instead (such
+ as twitter pictures), so they're url encoded and appended to the
+ proxy service.
+ """
+
+ host = match.group('host')
+
+ dest = SERVICES.get(host)
+ if dest is None:
+ return match.group(0)
+
+ destname, proxy = dest
+ replaced = self.config.get(destname, section='services')
+ result = replaced + match.group('rest')
+
+ if proxy:
+ url = urlquote(match.group(0))
+ result = replaced + url
+
+ # TODO: count parenthesis?
+ # Removes comma at the end of a link.
+ if result[-3] == '%2C':
+ result = result[:-3] + ','
+
+ return result
+
+ def handle_msg(self, msg: Message, tab: ChatTabs) -> None:
+ orig = msg['body']
+
+ if self.config.get('cleanup', section='default'):
+ msg['body'] = self.cleanup_url(msg['body'])
+ if self.config.get('redirect', section='default'):
+ msg['body'] = self.redirect_url(msg['body'])
+
+ if self.config.get('display_corrections', section='default') and \
+ msg['body'] != orig:
+ log.debug(
+ 'UntrackMe in tab \'%s\':\nOriginal: %s\nModified: %s',
+ tab.name, orig, msg['body'],
+ )
+
+ self.api.information(
+ 'UntrackMe in tab \'{}\':\nOriginal: {}\nModified: {}'.format(
+ tab.name, orig, msg['body']
+ ),
+ 'Info',
+ )
+
+ def cleanup_url(self, txt: str) -> str:
+ # fbclid: used globally (Facebook)
+ # utm_*: used globally https://en.wikipedia.org/wiki/UTM_parameters
+ # ncid: DoubleClick (Google)
+ # ref_src, ref_url: twitter
+ # Others exist but are excluded because they are not common.
+ # See https://en.wikipedia.org/wiki/UTM_parameters
+ return re.sub('(https?://[^ ]+)&?(fbclid|dclid|ncid|utm_source|utm_medium|utm_campaign|utm_term|utm_content|ref_src|ref_url)=[^ &#]*',
+ r'\1',
+ txt)
+
+ def redirect_url(self, txt: str) -> str:
+ return RE_URL.sub(self.map_services, txt)
diff --git a/plugins/upload.py b/plugins/upload.py
index db8615c2..6926c075 100644
--- a/plugins/upload.py
+++ b/plugins/upload.py
@@ -16,12 +16,15 @@ This plugin adds a command to the chat tabs.
"""
+
+from typing import Optional
+
import asyncio
import traceback
from os.path import expanduser
from glob import glob
-from slixmpp.plugins.xep_0363.http_upload import UploadServiceNotFound
+from slixmpp.plugins.xep_0363.http_upload import FileTooBig, HTTPError, UploadServiceNotFound
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
@@ -30,10 +33,20 @@ from poezio import tabs
class Plugin(BasePlugin):
+ dependencies = {'embed'}
+
def init(self):
+ self.embed = self.refs['embed']
+
if not self.core.xmpp['xep_0363']:
raise Exception('slixmpp XEP-0363 plugin failed to load')
- for _class in (tabs.PrivateTab, tabs.ConversationTab, tabs.MucTab):
+ if not self.core.xmpp['xep_0454']:
+ self.api.information(
+ 'slixmpp XEP-0454 plugin failed to load. '
+ 'Will not be able to encrypt uploaded files.',
+ 'Warning',
+ )
+ for _class in (tabs.PrivateTab, tabs.StaticConversationTab, tabs.DynamicConversationTab, tabs.MucTab):
self.api.add_tab_command(
_class,
'upload',
@@ -43,18 +56,29 @@ class Plugin(BasePlugin):
short='Upload a file',
completion=self.completion_filename)
- async def async_upload(self, filename):
+ async def upload(self, filename, encrypted=False) -> Optional[str]:
try:
- url = await self.core.xmpp['xep_0363'].upload_file(filename)
+ upload_file = self.core.xmpp['xep_0363'].upload_file
+ if encrypted:
+ upload_file = self.core.xmpp['xep_0454'].upload_file
+ url = await upload_file(filename)
except UploadServiceNotFound:
self.api.information('HTTP Upload service not found.', 'Error')
- return
+ return None
+ except (FileTooBig, HTTPError) as exn:
+ self.api.information(str(exn), 'Error')
+ return None
except Exception:
exception = traceback.format_exc()
self.api.information('Failed to upload file: %s' % exception,
'Error')
- return
- self.core.insert_input_text(url)
+ return None
+ return url
+
+ async def send_upload(self, filename, tab, encrypted=False):
+ url = await self.upload(filename, encrypted)
+ if url is not None:
+ self.embed.embed_image_url(url, tab)
@command_args_parser.quoted(1)
def command_upload(self, args):
@@ -63,7 +87,9 @@ class Plugin(BasePlugin):
return
filename, = args
filename = expanduser(filename)
- asyncio.ensure_future(self.async_upload(filename))
+ tab = self.api.current_tab()
+ encrypted = self.core.xmpp['xep_0454'] and tab.e2e_encryption is not None
+ asyncio.create_task(self.send_upload(filename, tab, encrypted))
@staticmethod
def completion_filename(the_input):
diff --git a/plugins/uptime.py b/plugins/uptime.py
index d5a07b7b..a55af970 100644
--- a/plugins/uptime.py
+++ b/plugins/uptime.py
@@ -12,8 +12,10 @@ Command
Retrieve the uptime of the server of ``jid``.
"""
from poezio.plugin import BasePlugin
-from poezio.common import parse_secs_to_str, safeJID
+from poezio.common import parse_secs_to_str
from slixmpp.xmlstream import ET
+from slixmpp import JID, InvalidJID
+from slixmpp.exceptions import IqError, IqTimeout
class Plugin(BasePlugin):
@@ -25,19 +27,23 @@ class Plugin(BasePlugin):
help='Ask for the uptime of a server or component (see XEP-0012).',
short='Get the uptime')
- def command_uptime(self, arg):
- def callback(iq):
- for query in iq.xml.getiterator('{jabber:iq:last}query'):
+ async def command_uptime(self, arg):
+ try:
+ jid = JID(arg)
+ except InvalidJID:
+ return
+ iq = self.core.xmpp.make_iq_get(ito=jid.server)
+ iq.append(ET.Element('{jabber:iq:last}query'))
+ try:
+ iq = await iq.send()
+ result = iq.xml.find('{jabber:iq:last}query')
+ if result is not None:
self.api.information(
'Server %s online since %s' %
(iq['from'], parse_secs_to_str(
- int(query.attrib['seconds']))), 'Info')
+ int(result.attrib['seconds']))), 'Info')
return
- self.api.information('Could not retrieve uptime', 'Error')
+ except (IqError, IqTimeout):
+ pass
+ self.api.information('Could not retrieve uptime', 'Error')
- jid = safeJID(arg)
- if not jid.server:
- return
- iq = self.core.xmpp.make_iq_get(ito=jid.server)
- iq.append(ET.Element('{jabber:iq:last}query'))
- iq.send(callback=callback)
diff --git a/plugins/user_extras.py b/plugins/user_extras.py
new file mode 100644
index 00000000..96559111
--- /dev/null
+++ b/plugins/user_extras.py
@@ -0,0 +1,634 @@
+"""
+This plugin enables rich presence events, such as mood, activity, gaming or tune.
+
+.. versionadded:: 0.14
+ This plugin was previously provided in the poezio core features.
+
+Command
+-------
+.. glossary::
+
+ /activity
+ **Usage:** ``/activity [<general> [specific] [comment]]``
+
+ Send your current activity to your contacts (use the completion to cycle
+ through all the general and specific possible activities).
+
+ Nothing means "stop broadcasting an activity".
+
+ /mood
+ **Usage:** ``/mood [<mood> [comment]]``
+ Send your current mood to your contacts (use the completion to cycle
+ through all the possible moods).
+
+ Nothing means "stop broadcasting a mood".
+
+ /gaming
+ **Usage:** ``/gaming [<game name> [server address]]``
+
+ Send your current gaming activity to your contacts.
+
+ Nothing means "stop broadcasting a gaming activity".
+
+
+Configuration
+-------------
+
+.. glossary::
+
+ display_gaming_notifications
+
+ **Default value:** ``true``
+
+ If set to true, notifications about the games your contacts are playing
+ will be displayed in the info buffer as 'Gaming' messages.
+
+ display_tune_notifications
+
+ **Default value:** ``true``
+
+ If set to true, notifications about the music your contacts listen to
+ will be displayed in the info buffer as 'Tune' messages.
+
+ display_mood_notifications
+
+ **Default value:** ``true``
+
+ If set to true, notifications about the mood of your contacts
+ will be displayed in the info buffer as 'Mood' messages.
+
+ display_activity_notifications
+
+ **Default value:** ``true``
+
+ If set to true, notifications about the current activity of your contacts
+ will be displayed in the info buffer as 'Activity' messages.
+
+ enable_user_activity
+
+ **Default value:** ``true``
+
+ Set this to ``false`` if you don’t want to receive the activity of your contacts.
+
+ enable_user_gaming
+
+ **Default value:** ``true``
+
+ Set this to ``false`` if you don’t want to receive the gaming activity of your contacts.
+
+ enable_user_mood
+
+ **Default value:** ``true``
+
+ Set this to ``false`` if you don’t want to receive the mood of your contacts.
+
+ enable_user_tune
+
+ **Default value:** ``true``
+
+ If this is set to ``false``, you will no longer be subscribed to tune events,
+ and the :term:`display_tune_notifications` option will be ignored.
+
+
+"""
+import asyncio
+from functools import reduce
+from typing import Dict
+
+from slixmpp import InvalidJID, JID, Message
+from poezio.decorators import command_args_parser
+from poezio.plugin import BasePlugin
+from poezio.roster import roster
+from poezio.contact import Contact, Resource
+from poezio.core.structs import Completion
+from poezio import common
+from poezio import tabs
+
+
+class Plugin(BasePlugin):
+
+ default_config = {
+ 'user_extras': {
+ 'display_gaming_notifications': True,
+ 'display_mood_notifications': True,
+ 'display_activity_notifications': True,
+ 'display_tune_notifications': True,
+ 'enable_user_activity': True,
+ 'enable_user_gaming': True,
+ 'enable_user_mood': True,
+ 'enable_user_tune': True,
+ }
+ }
+
+ def init(self):
+ for plugin in {'xep_0196', 'xep_0108', 'xep_0107', 'xep_0118'}:
+ self.core.xmpp.register_plugin(plugin)
+ self.api.add_command(
+ 'activity',
+ self.command_activity,
+ usage='[<general> [specific] [text]]',
+ help='Send your current activity to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting an activity".',
+ short='Send your activity.',
+ completion=self.comp_activity
+ )
+ self.api.add_command(
+ 'mood',
+ self.command_mood,
+ usage='[<mood> [text]]',
+ help='Send your current mood to your contacts '
+ '(use the completion). Nothing means '
+ '"stop broadcasting a mood".',
+ short='Send your mood.',
+ completion=self.comp_mood,
+ )
+ self.api.add_command(
+ 'gaming',
+ self.command_gaming,
+ usage='[<game name> [server address]]',
+ help='Send your current gaming activity to '
+ 'your contacts. Nothing means "stop '
+ 'broadcasting a gaming activity".',
+ short='Send your gaming activity.',
+ completion=None
+ )
+ handlers = [
+ ('user_mood_publish', self.on_mood_event),
+ ('user_tune_publish', self.on_tune_event),
+ ('user_gaming_publish', self.on_gaming_event),
+ ('user_activity_publish', self.on_activity_event),
+ ]
+ for name, handler in handlers:
+ self.core.xmpp.add_event_handler(name, handler)
+
+ def cleanup(self):
+ handlers = [
+ ('user_mood_publish', self.on_mood_event),
+ ('user_tune_publish', self.on_tune_event),
+ ('user_gaming_publish', self.on_gaming_event),
+ ('user_activity_publish', self.on_activity_event),
+ ]
+ for name, handler in handlers:
+ self.core.xmpp.del_event_handler(name, handler)
+ asyncio.create_task(self._stop())
+
+ async def _stop(self):
+ await asyncio.gather(
+ self.core.xmpp.plugin['xep_0108'].stop(),
+ self.core.xmpp.plugin['xep_0107'].stop(),
+ self.core.xmpp.plugin['xep_0196'].stop(),
+ )
+
+
+ @command_args_parser.quoted(0, 2)
+ async def command_mood(self, args):
+ """
+ /mood [<mood> [text]]
+ """
+ if not args:
+ return await self.core.xmpp.plugin['xep_0107'].stop()
+ mood = args[0]
+ if mood not in MOODS:
+ return self.core.information(
+ '%s is not a correct value for a mood.' % mood, 'Error')
+ if len(args) == 2:
+ text = args[1]
+ else:
+ text = None
+ await self.core.xmpp.plugin['xep_0107'].publish_mood(
+ mood, text
+ )
+
+ @command_args_parser.quoted(0, 3)
+ async def command_activity(self, args):
+ """
+ /activity [<general> [specific] [text]]
+ """
+ length = len(args)
+ if not length:
+ return await self.core.xmpp.plugin['xep_0108'].stop()
+
+ general = args[0]
+ if general not in ACTIVITIES:
+ return self.api.information(
+ '%s is not a correct value for an activity' % general, 'Error')
+ specific = None
+ text = None
+ if length == 2:
+ if args[1] in ACTIVITIES[general]:
+ specific = args[1]
+ else:
+ text = args[1]
+ elif length == 3:
+ specific = args[1]
+ text = args[2]
+ if specific and specific not in ACTIVITIES[general]:
+ return self.core.information(
+ '%s is not a correct value '
+ 'for an activity' % specific, 'Error')
+ await self.core.xmpp.plugin['xep_0108'].publish_activity(
+ general, specific, text
+ )
+
+ @command_args_parser.quoted(0, 2)
+ async def command_gaming(self, args):
+ """
+ /gaming [<game name> [server address]]
+ """
+ if not args:
+ return await self.core.xmpp.plugin['xep_0196'].stop()
+
+ name = args[0]
+ if len(args) > 1:
+ address = args[1]
+ else:
+ address = None
+ return await self.core.xmpp.plugin['xep_0196'].publish_gaming(
+ name=name, server_address=address
+ )
+
+ def comp_activity(self, the_input):
+ """Completion for /activity"""
+ n = the_input.get_argument_position(quoted=True)
+ args = common.shell_split(the_input.text)
+ if n == 1:
+ return Completion(
+ the_input.new_completion,
+ sorted(ACTIVITIES.keys()),
+ n,
+ quotify=True)
+ elif n == 2:
+ if args[1] in ACTIVITIES:
+ l = list(ACTIVITIES[args[1]])
+ l.remove('category')
+ l.sort()
+ return Completion(the_input.new_completion, l, n, quotify=True)
+
+ def comp_mood(self, the_input):
+ """Completion for /mood"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ return Completion(
+ the_input.new_completion,
+ sorted(MOODS.keys()),
+ 1,
+ quotify=True)
+
+ def on_gaming_event(self, message: Message):
+ """
+ Called when a pep notification for user gaming
+ is received
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ item = message['pubsub_event']['items']['item']
+ old_gaming = contact.rich_presence['gaming']
+ xml_node = item.xml.find('{urn:xmpp:gaming:0}game')
+ # list(xml_node) checks whether there are children or not.
+ if xml_node is not None and list(xml_node):
+ item = item['gaming']
+ # only name and server_address are used for now
+ contact.rich_presence['gaming'] = {
+ 'character_name': item['character_name'],
+ 'character_profile': item['character_profile'],
+ 'name': item['name'],
+ 'level': item['level'],
+ 'uri': item['uri'],
+ 'server_name': item['server_name'],
+ 'server_address': item['server_address'],
+ }
+ else:
+ contact.rich_presence['gaming'] = {}
+
+ if old_gaming != contact.rich_presence['gaming'] and self.config.get(
+ 'display_gaming_notifications'):
+ if contact.rich_presence['gaming']:
+ self.core.information(
+ '%s is playing %s' % (contact.bare_jid,
+ common.format_gaming_string(
+ contact.rich_presence['gaming'])), 'Gaming')
+ else:
+ self.core.information(contact.bare_jid + ' stopped playing.',
+ 'Gaming')
+
+ def on_mood_event(self, message: Message):
+ """
+ Called when a pep notification for a user mood
+ is received.
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ item = message['pubsub_event']['items']['item']
+ old_mood = contact.rich_presence.get('mood')
+ plugin = item.get_plugin('mood', check=True)
+ if plugin:
+ mood = item['mood']['value']
+ else:
+ mood = ''
+ if mood:
+ mood = MOODS.get(mood, mood)
+ text = item['mood']['text']
+ if text:
+ mood = '%s (%s)' % (mood, text)
+ contact.rich_presence['mood'] = mood
+ else:
+ contact.rich_presence['mood'] = ''
+
+ if old_mood != contact.rich_presence['mood'] and self.config.get(
+ 'display_mood_notifications'):
+ if contact.rich_presence['mood']:
+ self.core.information(
+ 'Mood from ' + contact.bare_jid + ': ' + contact.rich_presence['mood'],
+ 'Mood')
+ else:
+ self.core.information(
+ contact.bare_jid + ' stopped having their mood.', 'Mood')
+
+ def on_activity_event(self, message: Message):
+ """
+ Called when a pep notification for a user activity
+ is received.
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ item = message['pubsub_event']['items']['item']
+ old_activity = contact.rich_presence['activity']
+ xml_node = item.xml.find('{http://jabber.org/protocol/activity}activity')
+ # list(xml_node) checks whether there are children or not.
+ if xml_node is not None and list(xml_node):
+ try:
+ activity = item['activity']['value']
+ except ValueError:
+ return
+ if activity[0]:
+ general = ACTIVITIES.get(activity[0])
+ if general is None:
+ return
+ s = general['category']
+ if activity[1]:
+ s = s + '/' + general.get(activity[1], 'other')
+ text = item['activity']['text']
+ if text:
+ s = '%s (%s)' % (s, text)
+ contact.rich_presence['activity'] = s
+ else:
+ contact.rich_presence['activity'] = ''
+ else:
+ contact.rich_presence['activity'] = ''
+
+ if old_activity != contact.rich_presence['activity'] and self.config.get(
+ 'display_activity_notifications'):
+ if contact.rich_presence['activity']:
+ self.core.information(
+ 'Activity from ' + contact.bare_jid + ': ' +
+ contact.rich_presence['activity'], 'Activity')
+ else:
+ self.core.information(
+ contact.bare_jid + ' stopped doing their activity.',
+ 'Activity')
+
+ def on_tune_event(self, message: Message):
+ """
+ Called when a pep notification for a user tune
+ is received
+ """
+ contact = roster[message['from'].bare]
+ if not contact:
+ return
+ roster.modified()
+ item = message['pubsub_event']['items']['item']
+ old_tune = contact.rich_presence['tune']
+ xml_node = item.xml.find('{http://jabber.org/protocol/tune}tune')
+ # list(xml_node) checks whether there are children or not.
+ if xml_node is not None and list(xml_node):
+ item = item['tune']
+ contact.rich_presence['tune'] = {
+ 'artist': item['artist'],
+ 'length': item['length'],
+ 'rating': item['rating'],
+ 'source': item['source'],
+ 'title': item['title'],
+ 'track': item['track'],
+ 'uri': item['uri']
+ }
+ else:
+ contact.rich_presence['tune'] = {}
+
+ if old_tune != contact.rich_presence['tune'] and self.config.get(
+ 'display_tune_notifications'):
+ if contact.rich_presence['tune']:
+ self.core.information(
+ 'Tune from ' + message['from'].bare + ': ' +
+ common.format_tune_string(contact.rich_presence['tune']), 'Tune')
+ else:
+ self.core.information(
+ contact.bare_jid + ' stopped listening to music.', 'Tune')
+
+
+# Collection of mappings for PEP moods/activities
+# extracted directly from the XEP
+
+MOODS: Dict[str, str] = {
+ 'afraid': 'Afraid',
+ 'amazed': 'Amazed',
+ 'angry': 'Angry',
+ 'amorous': 'Amorous',
+ 'annoyed': 'Annoyed',
+ 'anxious': 'Anxious',
+ 'aroused': 'Aroused',
+ 'ashamed': 'Ashamed',
+ 'bored': 'Bored',
+ 'brave': 'Brave',
+ 'calm': 'Calm',
+ 'cautious': 'Cautious',
+ 'cold': 'Cold',
+ 'confident': 'Confident',
+ 'confused': 'Confused',
+ 'contemplative': 'Contemplative',
+ 'contented': 'Contented',
+ 'cranky': 'Cranky',
+ 'crazy': 'Crazy',
+ 'creative': 'Creative',
+ 'curious': 'Curious',
+ 'dejected': 'Dejected',
+ 'depressed': 'Depressed',
+ 'disappointed': 'Disappointed',
+ 'disgusted': 'Disgusted',
+ 'dismayed': 'Dismayed',
+ 'distracted': 'Distracted',
+ 'embarrassed': 'Embarrassed',
+ 'envious': 'Envious',
+ 'excited': 'Excited',
+ 'flirtatious': 'Flirtatious',
+ 'frustrated': 'Frustrated',
+ 'grumpy': 'Grumpy',
+ 'guilty': 'Guilty',
+ 'happy': 'Happy',
+ 'hopeful': 'Hopeful',
+ 'hot': 'Hot',
+ 'humbled': 'Humbled',
+ 'humiliated': 'Humiliated',
+ 'hungry': 'Hungry',
+ 'hurt': 'Hurt',
+ 'impressed': 'Impressed',
+ 'in_awe': 'In awe',
+ 'in_love': 'In love',
+ 'indignant': 'Indignant',
+ 'interested': 'Interested',
+ 'intoxicated': 'Intoxicated',
+ 'invincible': 'Invincible',
+ 'jealous': 'Jealous',
+ 'lonely': 'Lonely',
+ 'lucky': 'Lucky',
+ 'mean': 'Mean',
+ 'moody': 'Moody',
+ 'nervous': 'Nervous',
+ 'neutral': 'Neutral',
+ 'offended': 'Offended',
+ 'outraged': 'Outraged',
+ 'playful': 'Playful',
+ 'proud': 'Proud',
+ 'relaxed': 'Relaxed',
+ 'relieved': 'Relieved',
+ 'remorseful': 'Remorseful',
+ 'restless': 'Restless',
+ 'sad': 'Sad',
+ 'sarcastic': 'Sarcastic',
+ 'serious': 'Serious',
+ 'shocked': 'Shocked',
+ 'shy': 'Shy',
+ 'sick': 'Sick',
+ 'sleepy': 'Sleepy',
+ 'spontaneous': 'Spontaneous',
+ 'stressed': 'Stressed',
+ 'strong': 'Strong',
+ 'surprised': 'Surprised',
+ 'thankful': 'Thankful',
+ 'thirsty': 'Thirsty',
+ 'tired': 'Tired',
+ 'undefined': 'Undefined',
+ 'weak': 'Weak',
+ 'worried': 'Worried'
+}
+
+ACTIVITIES: Dict[str, Dict[str, str]] = {
+ 'doing_chores': {
+ 'category': 'Doing_chores',
+ 'buying_groceries': 'Buying groceries',
+ 'cleaning': 'Cleaning',
+ 'cooking': 'Cooking',
+ 'doing_maintenance': 'Doing maintenance',
+ 'doing_the_dishes': 'Doing the dishes',
+ 'doing_the_laundry': 'Doing the laundry',
+ 'gardening': 'Gardening',
+ 'running_an_errand': 'Running an errand',
+ 'walking_the_dog': 'Walking the dog',
+ 'other': 'Other',
+ },
+ 'drinking': {
+ 'category': 'Drinking',
+ 'having_a_beer': 'Having a beer',
+ 'having_coffee': 'Having coffee',
+ 'having_tea': 'Having tea',
+ 'other': 'Other',
+ },
+ 'eating': {
+ 'category': 'Eating',
+ 'having_breakfast': 'Having breakfast',
+ 'having_a_snack': 'Having a snack',
+ 'having_dinner': 'Having dinner',
+ 'having_lunch': 'Having lunch',
+ 'other': 'Other',
+ },
+ 'exercising': {
+ 'category': 'Exercising',
+ 'cycling': 'Cycling',
+ 'dancing': 'Dancing',
+ 'hiking': 'Hiking',
+ 'jogging': 'Jogging',
+ 'playing_sports': 'Playing sports',
+ 'running': 'Running',
+ 'skiing': 'Skiing',
+ 'swimming': 'Swimming',
+ 'working_out': 'Working out',
+ 'other': 'Other',
+ },
+ 'grooming': {
+ 'category': 'Grooming',
+ 'at_the_spa': 'At the spa',
+ 'brushing_teeth': 'Brushing teeth',
+ 'getting_a_haircut': 'Getting a haircut',
+ 'shaving': 'Shaving',
+ 'taking_a_bath': 'Taking a bath',
+ 'taking_a_shower': 'Taking a shower',
+ 'other': 'Other',
+ },
+ 'having_appointment': {
+ 'category': 'Having appointment',
+ 'other': 'Other',
+ },
+ 'inactive': {
+ 'category': 'Inactive',
+ 'day_off': 'Day_off',
+ 'hanging_out': 'Hanging out',
+ 'hiding': 'Hiding',
+ 'on_vacation': 'On vacation',
+ 'praying': 'Praying',
+ 'scheduled_holiday': 'Scheduled holiday',
+ 'sleeping': 'Sleeping',
+ 'thinking': 'Thinking',
+ 'other': 'Other',
+ },
+ 'relaxing': {
+ 'category': 'Relaxing',
+ 'fishing': 'Fishing',
+ 'gaming': 'Gaming',
+ 'going_out': 'Going out',
+ 'partying': 'Partying',
+ 'reading': 'Reading',
+ 'rehearsing': 'Rehearsing',
+ 'shopping': 'Shopping',
+ 'smoking': 'Smoking',
+ 'socializing': 'Socializing',
+ 'sunbathing': 'Sunbathing',
+ 'watching_a_movie': 'Watching a movie',
+ 'watching_tv': 'Watching tv',
+ 'other': 'Other',
+ },
+ 'talking': {
+ 'category': 'Talking',
+ 'in_real_life': 'In real life',
+ 'on_the_phone': 'On the phone',
+ 'on_video_phone': 'On video phone',
+ 'other': 'Other',
+ },
+ 'traveling': {
+ 'category': 'Traveling',
+ 'commuting': 'Commuting',
+ 'driving': 'Driving',
+ 'in_a_car': 'In a car',
+ 'on_a_bus': 'On a bus',
+ 'on_a_plane': 'On a plane',
+ 'on_a_train': 'On a train',
+ 'on_a_trip': 'On a trip',
+ 'walking': 'Walking',
+ 'cycling': 'Cycling',
+ 'other': 'Other',
+ },
+ 'undefined': {
+ 'category': 'Undefined',
+ 'other': 'Other',
+ },
+ 'working': {
+ 'category': 'Working',
+ 'coding': 'Coding',
+ 'in_a_meeting': 'In a meeting',
+ 'writing': 'Writing',
+ 'studying': 'Studying',
+ 'other': 'Other',
+ }
+}
diff --git a/plugins/vcard.py b/plugins/vcard.py
index 643dd569..b0c8e396 100644
--- a/plugins/vcard.py
+++ b/plugins/vcard.py
@@ -25,15 +25,16 @@ Command
vcard from the current interlocutor, and in the contact list to do it
on the currently selected contact.
"""
+import asyncio
from poezio.decorators import command_args_parser
from poezio.plugin import BasePlugin
from poezio.roster import roster
-from poezio.common import safeJID
from poezio.contact import Contact, Resource
from poezio.core.structs import Completion
from poezio import tabs
from slixmpp.jid import JID, InvalidJID
+from slixmpp.exceptions import IqTimeout
class Plugin(BasePlugin):
@@ -61,7 +62,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',
@@ -240,19 +241,18 @@ class Plugin(BasePlugin):
on_cancel = lambda form: self.core.close_tab()
self.core.open_new_form(form, on_cancel, on_validate)
- def _get_vcard(self, jid):
+ async def _get_vcard(self, jid):
'''Send an iq to ask the vCard.'''
-
- def timeout_cb(iq):
+ try:
+ vcard = await self.core.xmpp.plugin['xep_0054'].get_vcard(
+ jid=jid,
+ timeout=30,
+ )
+ self._handle_vcard(vcard)
+ except IqTimeout:
self.api.information('Timeout while retrieving vCard for %s' % jid,
'Error')
- return
- self.core.xmpp.plugin['xep_0054'].get_vcard(
- jid=jid,
- timeout=30,
- callback=self._handle_vcard,
- timeout_callback=timeout_cb)
@command_args_parser.raw
def command_vcard(self, arg):
@@ -266,14 +266,16 @@ class Plugin(BasePlugin):
self.api.information('Invalid JID: %s' % arg, 'Error')
return
- self._get_vcard(jid)
+ asyncio.create_task(
+ self._get_vcard(jid)
+ )
@command_args_parser.raw
def command_private_vcard(self, arg):
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,11 +284,15 @@ 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)
- self._get_vcard(jid)
+ try:
+ jid = JID(arg)
+ except InvalidJID:
+ return self.api.information('Invalid JID: %s' % arg, 'Error')
+ asyncio.create_task(
+ self._get_vcard(jid)
+ )
@command_args_parser.raw
def command_roster_vcard(self, arg):
@@ -295,9 +301,13 @@ class Plugin(BasePlugin):
return
current = self.api.current_tab().selected_row
if isinstance(current, Resource):
- self._get_vcard(JID(current.jid).bare)
+ asyncio.create_task(
+ self._get_vcard(JID(current.jid).bare)
+ )
elif isinstance(current, Contact):
- self._get_vcard(current.bare_jid)
+ asyncio.create_task(
+ self._get_vcard(current.bare_jid)
+ )
def completion_vcard(self, the_input):
contacts = [contact.bare_jid for contact in roster.get_contacts()]