Poezio theme

This commit is contained in:
Kujiu 2023-09-03 18:26:58 +02:00
parent fa3f668f6b
commit 1e334f20f1
Signed by: kujiu
GPG Key ID: ABBB2CAC6855599F
74 changed files with 301 additions and 6983 deletions

View File

@ -36,6 +36,7 @@ You need to install:
- offlineimap3
- pandoc
- poezio
- pyinotify
- tmux
- toot
- unoconv

View File

@ -63,3 +63,5 @@ vim.keymap.set('v', 'gL', ":'<,'>!lou_translate --backward en-us-brf.dis,fr-bfu-
vim.cmd 'colorscheme nightfox'
vim.transparent_window = true
vim.cmd 'highlight Normal ctermbg=none guibg=none'

View File

@ -7,7 +7,7 @@ bind-key -T copy-mode-vi C-v send -X rectangle-toggle
bind-key -T copy-mode-vi y send -X copy-pipe-and-cancel
set -g lock-command vlock
set -g lock-after-time 0
set -g lock-after-time 900
bind l lock-client
bind L lock-session

View File

@ -12,7 +12,7 @@
# like room administration, nickname registration.
# The 'server' option will be ignored if you specify a JID (Jabber identifier)
# It should be in the form nickname@server.tld or nickname@server.tld/resource
jid = template@example.com
jid = REPLACE_JID
# A password is needed only if you specified a jid. It will be ignored otherwise
# If you leave this empty, the password will be asked at each startup
@ -20,11 +20,11 @@ jid = template@example.com
# A command that will be executed if "password" is not set, e.g. a session password
# manager like secret-tool on gnome, or anything you want
eval_password = keyring get "xmpp:template@example.com" "template@example.com"
eval_password = keyring get "KEYRING_ID" "KEYRING_USER"
# This identifies this client over time with the server, to let it optimise
# offline storage and various other features.
device_id = REPLACEDEVICEHOSTNAME
device_id = REPLACE_DEVICE_HOSTNAME
# Path to a PEM certificate file to use for certificate authentication
# through SASL External. If set, keyfile MUST be provided as well in
@ -50,7 +50,7 @@ rooms =
highlight_on =
# Colon-separated list of plugins to load on startup
plugins_autoload = omemo:autocorrect:user_extras:status:dice:link:quote:reorder:vcard:upload:screen_detach:qr:close_all:contact:disco:display_corrections:irc:link:mpd_client
plugins_autoload = omemo:autocorrect:user_extras:status:dice:link:quote:reorder:vcard:upload:screen_detach:qr:close_all:contact:disco:display_corrections:irc:link:mpd_client:change_title
# The server used for anonymous connection.
# Make sure the server you're using accepts anonymous authentication
@ -58,7 +58,7 @@ plugins_autoload = omemo:autocorrect:user_extras:status:dice:link:quote:reorder:
# TLS Certificate fingerprint
# Do not touch this if you dont know what you are doing
certificate = F5:9A:6D:C9:C5:1A:B5:05:22:F4:EB:4E:23:32:7C:97:6A:24:FF:F8:BA:8D:7F:C1:D1:E8:6F:37:EE:A3:FE:43
certificate = REPLACE_FINGERPRINT
# List of ciphers allowed when connecting to the server,
# this list prioritizes forward secrecy and forbids anything
@ -194,9 +194,9 @@ enable_xhtml_im = true
# - status changes won't be displayed unless
# the user talked in the last 2 minutes
#hide_exit_join = -1
hide_exit_join = 600
#hide_status_change = 120
hide_status_change = 120
# Some informational messages (error, a contact getting connected, etc)
@ -298,6 +298,11 @@ hide_user_list = true
# - false or anything else: no highlighting
#show_composing_tabs = direct
# Use deterministic coloration for tab names or tab numbers in the activity bar.
# Consistent Color Generation (XEP-0392).
autocolor_tab_names = false
# Ignore private messages received in chatrooms
#ignore_private = false
@ -381,7 +386,7 @@ roster_show_offline = true
# in the theme_dir directory.
# If the file is not found (or no filename is specified) the default
# theme will be used instead
#theme = default
theme = Nightfox
# Whether to create gaps when moving or closing a tab
# (a gap means that the number of your tabs does not depend of the previous tabs
@ -533,9 +538,8 @@ M-t = _exc_toggle show_timestamps
# You should not edit this section, it is just used by poezio
# to save various data across restarts
folded_roster_groups =
info_win_height = 2
info_win_height = 0
[muc_colors]
# Set color for a nick, under the form
# nick = color
[reorder]

View File

@ -1 +0,0 @@
Plugins from poezio directory

View File

@ -1,139 +0,0 @@
"""
This plugin adds several convenient aliases, to shorten
roles/affiliation management.
Aliases defined
---------------
All those commands take a nick or a JID as a parameter.
For roles
~~~~~~~~~
.. glossary::
:sorted:
/visitor
/mute
Set the role to ``visitor``
/participant
Set the role to ``participant``
/moderator
/op
Set the role to ``moderator``
For affiliations
~~~~~~~~~~~~~~~~
.. glossary::
:sorted:
/admin
Set the affiliation to ``admin``
/member
/voice
Set the affiliation to ``member``
/noaffiliation
Set the affiliation to ``none``
/owner
Set the affiliation to ``owner``
"""
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
from poezio.core.structs import Completion
class Plugin(BasePlugin):
"""
Adds several convenient aliases to /affiliation and /role:
/visitor
/participant
/moderator == /op
/member == /voice
/owner
/admin
/noaffiliation
"""
def init(self):
for role in ('visitor', 'participant', 'moderator'):
self.api.add_tab_command(
MucTab,
role,
self.role(role),
help='Set the role of a nick to %s' % role,
usage='<nick>',
short='Set the role to %s' % role,
completion=self.complete_nick)
for aff in ('member', 'owner', 'admin'):
self.api.add_tab_command(
MucTab,
aff,
self.affiliation(aff),
usage='<nick>',
help='Set the affiliation of a nick to %s' % aff,
short='Set the affiliation to %s' % aff,
completion=self.complete_nick)
self.api.add_tab_command(
MucTab,
'noaffiliation',
self.affiliation('none'),
usage='<nick>',
help='Set the affiliation of a nick to none.',
short='Set the affiliation to none.',
completion=self.complete_nick)
self.api.add_tab_command(
MucTab,
'voice',
self.affiliation('member'),
usage='<nick>',
help='Set the affiliation of a nick to member.',
short='Set the affiliation to member.',
completion=self.complete_nick)
self.api.add_tab_command(
MucTab,
'op',
self.role('moderator'),
usage='<nick>',
help='Set the role of a nick to moderator.',
short='Set the role to moderator.',
completion=self.complete_nick)
self.api.add_tab_command(
MucTab,
'mute',
self.role('visitor'),
usage='<nick>',
help='Set the role of a nick to visitor.',
short='Set the role to visitor.',
completion=self.complete_nick)
def role(self, role):
async def inner(args):
await self.api.current_tab().command_role(args + ' ' + role)
return inner
def affiliation(self, 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()
compare_users = lambda x: x.last_talked
word_list = [user.nick for user in sorted(tab.users, key=compare_users, reverse=True)\
if user.nick != tab.own_nick]
return Completion(the_input.auto_completion, word_list, '')

View File

@ -1,207 +0,0 @@
"""
Usage
-----
This plugin defines two new global commands: :term:`/alias` and :term:`/unalias`.
.. glossary::
/alias
**Usage:** ``/alias <name> <command> [args]``
This command will create a new command, named ``<name>`` (and callable
with ``/name``), that runs ``/command``, with ``[args]`` as fixed
args for the command.
When you run the alias, you can also pass parameters to it, that will be
given to the original command.
Example: ::
/alias toto say koin
Will bind ``/say koin`` to ``/toto``, so this alias will work in any
Chat tab. If someone calls it with ::
/toto koin
Poezio will then execute ``/say koin koin``.
Also, you can rebind arguments arbitrarily, with the ``{}`` placeholder.
For example, ::
/alias toto say {} le {}
/toto loulou coucou
Will execute ``/say loulou le coucou``, because the ``{}`` are
replaced with the command args, in the order they are given.
Extra args are still added at the end of the command if provided
(args used for the formatting are only used for the formatting).
/unalias
**Usage:** ``/unalias <name>``
This command removes a defined alias.
Config
------
The aliases are stored inside the configuration file for the plugin.
You can either use the above commands or write it manually, and it
will be read when the plugin is loaded.
Example of the syntax:
.. code-block:: ini
[alias]
toto = say {} le {}
j = join {}@conference.jabber.org/nick
jp = say je proteste
"""
from poezio.plugin import BasePlugin
from poezio.common import shell_split
from poezio.core.structs import Completion
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'alias',
self.command_alias,
usage='<alias> <command> [args]',
short='Create an alias command',
help='Create an alias for <command> with [args].')
self.api.add_command(
'unalias',
self.command_unalias,
usage='<alias>',
help='Remove a previously created alias',
short='Remove an alias',
completion=self.completion_unalias)
self.commands = {}
self.load_conf()
def load_conf(self):
"""
load stored aliases on startup
"""
for alias in self.config.options():
full = self.config.get(alias, '')
if full:
self.command_alias(alias + ' ' + full, silent=True)
def command_alias(self, line, silent=False):
"""
/alias <alias> <command> [args]
"""
arg = split_args(line)
if not arg:
if not silent:
self.api.information('Alias: Not enough parameters', 'Error')
return
alias, command, args = arg
if alias in self.commands:
update = True
elif alias in self.core.commands:
if not silent:
self.api.information('Alias: command already exists', 'Error')
return
else:
update = False
self.config.set(alias, command + ' ' + args)
self.commands[alias] = command_wrapper(
generic_command, lambda: self.get_command(command), args)
self.api.del_command(alias)
self.api.add_command(
alias, self.commands[alias],
'This command is an alias for /%s %s' % (alias, command))
if not silent:
if update:
self.api.information('Alias /%s updated' % alias, 'Info')
else:
self.api.information('Alias /%s successfully created' % alias,
'Info')
def command_unalias(self, alias):
"""
/unalias <existing alias>
"""
if alias in self.commands:
del self.commands[alias]
self.api.del_command(alias)
self.config.remove(alias)
self.api.information('Alias /%s successfully deleted' % alias,
'Info')
def completion_unalias(self, the_input):
"Completion for /unalias"
aliases = [alias for alias in self.commands]
aliases.sort()
return Completion(
the_input.auto_completion, aliases, '', quotify=False)
def get_command(self, name):
"""Returns the function associated with a command"""
def dummy(args):
"""Dummy function called if the command doesnt exist"""
pass
if name in self.commands:
return dummy
elif name in self.core.commands:
return self.core.commands[name].func
elif name in self.api.current_tab().commands:
return self.api.current_tab().commands[name].func
return dummy
def split_args(line):
"""
Extract the relevant vars from the command line
"""
arg = line.split()
if len(arg) < 2:
return None
alias_pos = line.find(' ')
alias = line[:alias_pos]
end = line[alias_pos + 1:]
args_pos = end.find(' ')
if args_pos == -1:
command = end
args = ''
else:
command = end[:args_pos]
args = end[args_pos + 1:]
return (alias, command, args)
def generic_command(command, extra_args, args):
"""
Function that will execute the command and set the relevant
parameters (format string, etc).
"""
args = shell_split(args)
new_extra_args = extra_args.format(*args)
count = extra_args.count('{}')
args = args[count:]
new_extra_args += ' '.join(args)
return command()(new_extra_args)
def command_wrapper(func, command, extra_args):
"set the predefined arguments"
def wrapper(*args, **kwargs):
return func(command, extra_args, *args, **kwargs)
return wrapper

View File

@ -1,35 +0,0 @@
"""
This plugin broadcasts a message to all your joined rooms.
.. note:: With great power comes great responsibility.
Use with moderation.
Command
-------
.. glossary::
/amsg
**Usage:** ``/amsg <message>``
Broadcast a message.
"""
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'amsg',
self.command_amsg,
usage='<message>',
short='Broadcast a message',
help='Broadcast the message to all the joined rooms.')
async def command_amsg(self, args):
for room in self.core.tabs:
if isinstance(room, MucTab) and room.joined:
await room.command_say(args)

View File

@ -1,97 +0,0 @@
"""
This plugin lets you perform simple replacements on the last message.
Usage
-----
.. note:: the ``/``, ``#``, ``!``, ``:`` and ``;`` chars can be used as separators,
even if the examples only use ``/``
Regex replacement
~~~~~~~~~~~~~~~~~
Once the plugin is loaded, any message matching the following regex::
^s/(.+?)/(.*?)(/|/g)?$
will be interpreted as a regex replacement, and the substitution will be
applied to the last sent message.
For example, if you sent the message::
This tab lists all public rooms on a MUC service. It is currently very limited but will be improved in the future. There currently is no way to search a room.
And you now want to replace MUC with multi-user chat, you input::
s/MUC/multi-user chat
And poezio will correct the message for you.
Raw string replacement
~~~~~~~~~~~~~~~~~~~~~~
Once the plugin is loaded, any message matching the following regex::
^r/(.+?)/(.*?)(/|/g)?$
will be interpreted as a replacement, and the substitution will be applied
to the last send message.
This variant is useful if you dont want to care about regular expressions
(and you do not want to have to escape stuff like space or backslashes).
"""
from poezio.plugin import BasePlugin
import re
allowed_separators = '/#!:;'
sed_re = re.compile(
'^([sr])(?P<sep>[%s])(.+?)(?P=sep)(.*?)((?P=sep)|(?P=sep)g)?$' %
allowed_separators)
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.sed_fix)
self.api.add_event_handler('conversation_say', self.sed_fix)
self.api.add_event_handler('private_say', self.sed_fix)
def sed_fix(self, msg, tab):
if not tab.last_sent_message:
return
if 'correct' not in tab.commands:
return
body = tab.last_sent_message['body']
match = sed_re.match(msg['body'])
if not match:
return
typ, sep, remove, put, matchall = match.groups()
replace_all = False
if matchall == sep + 'g':
replace_all = True
if typ == 's':
try:
if replace_all:
new_body = re.sub(remove, put, body)
else:
new_body = re.sub(remove, put, body, count=1)
except Exception as e:
self.api.information(
'Invalid regex for the autocorrect '
'plugin: %s' % e, 'Error')
return
elif typ == 'r':
if replace_all:
new_body = body.replace(remove, put)
else:
new_body = body.replace(remove, put, 1)
if body != new_body:
msg['body'] = new_body
msg['replace']['id'] = tab.last_sent_message['id']

View File

@ -1,70 +0,0 @@
#! /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()

View File

@ -1,83 +0,0 @@
"""
This plugin sends a small image to the recipient of your choice, using XHTML-IM and Bits of Binary.
Usage
-----
/bob some/image.png
Configuration options
---------------------
.. glossary::
max_size
**Default:** ``2048``
The maximum acceptable size of a file, over which you will get an error instead.
max_age
**Default:** ``86400``
The time during which the file should stay in cache on the receiving side.
"""
from poezio.core.structs import Completion
from poezio.plugin import BasePlugin
from poezio import tabs
from pathlib import Path
from glob import glob
from os.path import expanduser
from mimetypes import guess_type
class Plugin(BasePlugin):
default_config = {'bob': {'max_size': 2048, 'max_age': 86400}}
def init(self):
for tab in tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab, tabs.MucTab:
self.api.add_tab_command(
tab,
'bob',
self.command_bob,
usage='<image>',
help='Send image <image> to the current discussion',
short='Send a short image',
completion=self.completion_bob)
async def command_bob(self, filename):
path = Path(expanduser(filename))
try:
size = path.stat().st_size
except OSError as exc:
self.api.information('Error sending “%s”: %s' % (path.name, exc),
'Error')
return
mime_type = guess_type(path.as_posix())[0]
if mime_type is None or not mime_type.startswith('image/'):
self.api.information(
'Error sending “%s”, not an image file.' % path.name, 'Error')
return
if size > self.config.get('max_size'):
self.api.information(
'Error sending “%s”, file too big.' % path.name, 'Error')
return
with open(path.as_posix(), 'rb') as file:
data = file.read()
max_age = self.config.get('max_age')
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))
@staticmethod
def completion_bob(the_input):
txt = expanduser(the_input.get_text()[5:])
images = []
for filename in glob(txt + '*'):
mime_type = guess_type(filename)[0]
if mime_type is not None and mime_type.startswith('image/'):
images.append(filename)
return Completion(the_input.auto_completion, images, quotify=False)

View File

@ -1,15 +0,0 @@
"""
Once loaded, everything you will send will be IN CAPITAL LETTERS.
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.caps)
self.api.add_event_handler('conversation_say', self.caps)
self.api.add_event_handler('private_say', self.caps)
def caps(self, msg, tab):
msg['body'] = xhtml.clean_text(msg['body']).upper()

View File

@ -1,21 +0,0 @@
"""
This plugin will set the title of your terminal to the name of the current tab.
"""
from poezio.plugin import BasePlugin
import sys
class Plugin(BasePlugin):
def init(self):
self.on_tab_change(None, new_tab=self.core.tabs.current_tab)
self.api.add_event_handler('tab_change', self.on_tab_change)
def cleanup(self):
"Re-set the terminal title to 'poezio'"
sys.stdout.write("\x1b]0;poezio\x07")
sys.stdout.flush()
def on_tab_change(self, old_tab, new_tab):
sys.stdout.write("\x1b]0;{}\x07".format(new_tab.name))
sys.stdout.flush()

View File

@ -1,44 +0,0 @@
"""
``close_all`` plugin: close all tabs except chatrooms and the contact list.
Commands
--------
.. glossary::
/closeall
**Usage:** ``/closeall``
Close all tabs except the roster and chatroom tabs.
"""
from poezio.plugin import BasePlugin
from poezio import tabs
from poezio.decorators import command_args_parser
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'closeall',
self.command_closeall,
help='Close all non-chatroom tabs.')
@command_args_parser.ignored
def command_closeall(self):
"""
/closeall
"""
current = self.api.current_tab()
if not isinstance(current, (tabs.RosterInfoTab, tabs.MucTab)):
self.core.go_to_roster()
current = self.api.current_tab()
def filter_func(x):
return not isinstance(x, (tabs.RosterInfoTab, tabs.MucTab))
matching_tabs = list(filter(filter_func, self.core.tabs.get_tabs()))
length = len(matching_tabs)
for tab in matching_tabs:
self.core.close_tab(tab)
self.api.information('%s tabs closed.' % length, 'Info')
self.core.refresh_window()

View File

@ -1,52 +0,0 @@
"""
This plugin adds a :term:`/code` command, to send syntax highlighted snippets
of code using pygments and XHTML-IM (XEP-0071).
Install
-------
Either use your distribution tools to install python3-pygments or equivalent,
or run:
.. code-block:: shell
pip install --user pygments
Usage
-----
.. glossary::
/code <language> <snippet>
Run this command to send the <snippet> of code, syntax highlighted
using pygmentss <language> lexer.
"""
from poezio.plugin import BasePlugin
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter #pylint: disable=no-name-in-module
FORMATTER = HtmlFormatter(nowrap=True, noclasses=True)
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'code',
self.command_code,
usage='<language> <code>',
short='Sends syntax-highlighted code',
help='Sends syntax-highlighted code in the current tab')
def command_code(self, args):
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)
tab = self.api.current_tab()
code = highlight(code, lexer, FORMATTER)
tab.command_xhtml('<pre><code class="language-%s">%s</code></pre>' % (language, code.rstrip('\n')))

View File

@ -1,60 +0,0 @@
"""
Do a disco#info query on a JID, display the XEP-0157 Contact Addresses
Usage
-----
.. glossary::
/contact
**Usage:** ``/contact <JID>``
This command queries a JID for its Contact Addresses.
"""
from poezio.plugin import BasePlugin
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.jid import InvalidJID
CONTACT_TYPES = ['abuse', 'admin', 'feedback', 'sales', 'security', 'support']
class Plugin(BasePlugin):
def init(self):
self.api.add_command('contact', self.command_disco,
usage='<JID>',
short='Get the Contact Addresses of a JID',
help='Get the Contact Addresses of a JID')
def on_disco(self, iq):
info = iq['disco_info']
contacts = []
# 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), 'Contact Info')
else:
self.api.information(f'No Contact Addresses for {iq["from"]}', 'Error')
async def command_disco(self, jid):
try:
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')

View File

@ -1,51 +0,0 @@
"""
This plugin lets you set the CSI_ state manually, when the autoaway plugin
is not sufficient for your usage.
Commands
--------
.. glossary::
/csi_active
**Usage:** ``/csi_active``
Set CSI state to ``active``.
/csi_inactive
**Usage:** ``/csi_inactive``
Set CSI state to ``inactive``.
.. _CSI: https://xmpp.org/extensions/xep-0352.html
"""
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'csi_active',
self.command_active,
help='Set the client state indication to “active”',
short='Manual set active')
self.api.add_command(
'csi_inactive',
self.command_inactive,
help='Set the client state indication to “inactive”',
short='Manual set inactive')
def command_active(self, args):
if not self.core.xmpp.plugin['xep_0352'].enabled:
self.api.information('CSI is not enabled in this server',
'Warning')
else:
self.core.xmpp.plugin['xep_0352'].send_active()
def command_inactive(self, args):
if not self.core.xmpp.plugin['xep_0352'].enabled:
self.api.information('CSI is not enabled in this server',
'Warning')
else:
self.core.xmpp.plugin['xep_0352'].send_inactive()

View File

@ -1,42 +0,0 @@
"""
This plugin adds a "cyber" prefix to a random word in your chatroom messages.
Usage
-----
Say something in a MUC tab.
Configuration options
---------------------
.. glossary::
frequency
**Default:** ``10``
The percentage of the time the plugin will activate (randomly). 100 for every message, <= 0 for never.
"""
from poezio.plugin import BasePlugin
from random import choice, randint
import re
DEFAULT_CONFIG = {'cyber': {'frequency': 10}}
class Plugin(BasePlugin):
default_config = DEFAULT_CONFIG
def init(self):
self.api.add_event_handler('muc_say', self.cyberize)
def cyberize(self, msg, tab):
if randint(1, 100) > self.config.get('frequency'):
return
words = [
word for word in re.split('\W+', msg['body']) if len(word) > 3
]
if words:
word = choice(words)
msg['body'] = msg['body'].replace(word, 'cyber' + word)

View File

@ -1,37 +0,0 @@
"""
This plugin adds a message at 00:00 in each of your chat tabs saying that the
date has changed.
"""
import datetime
from gettext import gettext as _
from poezio import timed_events, tabs
from poezio.plugin import BasePlugin
from poezio.ui.types import InfoMessage
class Plugin(BasePlugin):
def init(self):
self.schedule_event()
def cleanup(self):
self.api.remove_timed_event(self.next_event)
def schedule_event(self):
day_change = datetime.datetime.combine(datetime.date.today(),
datetime.time())
day_change += datetime.timedelta(1)
self.next_event = timed_events.TimedEvent(day_change, self.day_change)
self.api.add_timed_event(self.next_event)
def day_change(self):
msg = _("Day changed to %s") % (datetime.date.today().isoformat())
for tab in self.core.tabs:
if isinstance(tab, tabs.ChatTab):
tab.add_message(InfoMessage(msg))
self.core.refresh_window()
self.schedule_event()

View File

@ -1,122 +0,0 @@
"""
Dice plugin: roll some dice
Usage of this plugin is not recommended.
Commands
--------
.. glossary::
/roll [number of dice] [duration of the roll]
Roll one or several unicode dice
Configuration
-------------
.. glossary::
:sorted:
refresh
**Default:** ``0.5``
Interval in seconds between each correction (the closest to 0 is the fastest)
default_duration
**Default:** ``5``
Total duration of the animation.
"""
import random
from typing import Optional
from poezio import tabs
from poezio.decorators import command_args_parser
from poezio.plugin import BasePlugin
DICE = '\u2680\u2681\u2682\u2683\u2684\u2685'
class DiceRoll:
__slots__ = [
'duration', 'total_duration', 'dice_number', 'msgtype', '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 = msgtype
self.jid = jid
self.msgid = msgid
self.increments = increments
def reroll(self):
self.duration += self.increments
def is_finished(self):
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.75, "default_duration": 7.5}}
def init(self):
for tab_t in [tabs.MucTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab]:
self.api.add_tab_command(
tab_t,
'roll',
self.command_dice,
help='Roll a die',
usage='[number] [duration]')
@command_args_parser.quoted(0, 2, ['', ''], True)
def command_dice(self, args):
tab = self.api.current_tab()
duration = self.config.get('default_duration')
num_dice = 1
try:
if args[0]:
num_dice = int(args[0])
if args[1]:
duration = float(args[1])
except ValueError:
self.core.command.help("roll")
return
else:
if num_dice <= 0 or duration < 0:
self.core.command.help("roll")
return
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')
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)
def delayed_event(self, roll):
if roll.is_finished():
return
roll.reroll()
message = self.core.xmpp.make_message(roll.jid)
message["type"] = roll.msgtype
message["body"] = roll_dice(roll.dice_number)
message["replace"]["id"] = roll.msgid
message.send()
event = self.api.create_delayed_event(roll.increments,
self.delayed_event, roll)
self.api.add_timed_event(event)

View File

@ -1,106 +0,0 @@
"""
Do a disco#info query on a JID
Usage
-----
.. glossary::
/disco
**Usage:** ``/disco <JID>``
This command queries a JID for its disco#info.
There is no cache, as this is generally used for debug more than
anything user-related.
"""
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):
def init(self):
self.api.add_command(
'disco',
self.command_disco,
usage='<JID> [node] [info|items]',
short='Get the disco#info of a JID',
help='Get the disco#info of a JID')
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')
features = sorted(str(feature) for feature in info['features'])
self.api.information('\n'.join(features), 'Features')
title = 'Server Info'
server_info = []
for field in info['form']:
var = field['var']
if field['type'] == 'hidden' and var == 'FORM_TYPE':
title = field['value'][0]
continue
sep = '\n ' + len(var) * ' '
field_value = field.get_value(convert=False)
value = sep.join(field_value) if isinstance(field_value,
list) else field_value
server_info.append('%s: %s' % (var, value))
if server_info:
self.api.information('\n'.join(server_info), title)
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:
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')

View File

@ -1,84 +0,0 @@
"""
Lists old versions of a corrected message.
Usage
-----
.. glossary::
/display_corrections
**Usage:** ``/display_corrections [number]``
This command lists the old versions of a message.
Without argument, it will list the last corrected message if there
is any. If you give an integer as an argument, ``/display_corrections``
will go back gradually in the buffer to find the message matching
that number (starting from 1, for the last corrected message).
If you are scrolling in the buffer, Poezio will list the corrected messages
starting from the first you can see. (although there are some problems with
multiline messages).
"""
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.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'display_corrections',
handler=self.command_display_corrections,
usage='<number>',
help=
'Display all the corrections of the number-th last corrected message.',
short='Display the corrections of a message')
def find_corrected(self, nb):
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in reversed(messages):
if not isinstance(message, Message):
continue
if message.old_message:
if nb == 1:
return message
else:
nb -= 1
return None
def command_display_corrections(self, args):
theme = get_theme()
args = shell_split(args)
if len(args) == 1:
try:
nb = int(args[0])
except:
return self.api.run_command('/help display_corrections')
else:
nb = 1
message = self.find_corrected(nb)
if message:
display = []
while message:
str_time = message.time.strftime(theme.SHORT_TIME_FORMAT)
display.append('%s %s%s%s %s' %
(str_time, '* '
if message.me else '', message.nickname, ''
if message.me else '>', message.txt))
message = message.old_message
self.api.information(
'Older versions:\n' + '\n'.join(display[::-1]), 'Info')
else:
self.api.information('No corrected message found.', 'Warning')
def cleanup(self):
del self.config

View File

@ -1,14 +0,0 @@
"""
Double the first word of any message you send in a :ref:`muctab`, making you appear retarded.
"""
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.double)
def double(self, msg, tab):
split = msg['body'].split()
if split:
msg['body'] = split[0] + ' ' + msg['body']

View File

@ -1,50 +0,0 @@
"""
Display an image URL as an embedded image in some clients like Conversations.
Uses: https://xmpp.org/extensions/xep-0066.html#x-oob
Usage
-----
.. glossary::
/embed <image_url>
Run this command to send the <image_url> as an
embedded image in your contact's client.
"""
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.StaticConversationTab, tabs.DynamicConversationTab, tabs.PrivateTab]:
self.api.add_tab_command(
tab_t,
'embed',
self.embed_image_url,
help='Embed an image url into the contact\'s client',
usage='<image_url>')
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(
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()

View File

@ -1,60 +0,0 @@
# 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'])

View File

@ -1,98 +0,0 @@
"""
This plugin lets you execute a system command through poezio.
Usage
-----
.. warning:: Running commands that start a daemon or an interface is not a good
idea.
.. glossary::
/exec
**Usage:** ``/exec [-o|-O] <command>``
Execute a system command.
::
/exec command
Will give you the result in the information buffer.
::
/exec -o command
Will send the result of the command into the current tab, if possible.
::
/exec -O command
Will send the result of the command and the command summary into the current
tab, if possible.
"""
from poezio.plugin import BasePlugin
from poezio import common
import asyncio
import subprocess
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'exec',
self.command_exec,
usage='[-o|-O] <command>',
help=
'Execute a shell command and prints the result in the information buffer. The command should be ONE argument, that means it should be between \"\". The first argument (before the command) can be -o or -O. If -o is specified, it sends the result in the current conversation. If -O is specified, it sends the command and its result in the current conversation.\nExample: /exec -O \"uptime\" will send “uptime\n20:36:19 up 3:47, 4 users, load average: 0.09, 0.13, 0.09” in the current conversation.',
short='Execute a command')
async def async_exec(self, command, arg):
create = asyncio.create_subprocess_exec(
'sh',
'-c',
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
process = await create
except OSError as e:
self.api.information('Failed to execute command: %s' % (e, ),
'Error')
return
stdout, stderr = await process.communicate()
result = stdout.decode('utf-8')
stderr = stderr.decode('utf-8')
if arg == '-o':
if not self.api.send_message('%s' % (result, )):
self.api.information(
'Cannot send result (%s), this is not a conversation tab' %
result, 'Error')
elif arg == '-O':
if not self.api.send_message('%s:\n%s' % (command, result)):
self.api.information(
'Cannot send result (%s), this is not a conversation tab' %
result, 'Error')
else:
self.api.information('%s:\n%s' % (command, result), 'Info')
if stderr:
self.api.information('stderr for %s:\n%s' % (command, stderr),
'Info')
await process.wait()
def command_exec(self, args):
args = common.shell_split(args)
if len(args) == 1:
command = args[0]
arg = None
elif len(args) == 2:
command = args[1]
arg = args[0]
else:
self.api.run_command('/help exec')
return
asyncio.create_task(self.async_exec(command, arg))

View File

@ -1,48 +0,0 @@
"""
This plugin uses figlet to transform every message into a big ascii-art
message.
Usage
-----
Say something in a Chat tab.
.. note:: Can create fun things when used with :ref:`The rainbow plugin <rainbow-plugin>`.
"""
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(
['figlet', '--', msg['body']], stdout=subprocess.PIPE)
result = process.communicate()[0].decode('utf-8')
msg['body'] = result

View File

@ -1,19 +0,0 @@
"""
Show the exchanged IQs (useful for debugging).
"""
from poezio.plugin import BasePlugin
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream.handler import Callback
class Plugin(BasePlugin):
def init(self):
self.core.xmpp.register_handler(
Callback('Iq_show', StanzaPath('iq'), self.handle_iq))
def handle_iq(self, iq):
self.api.information('%s' % iq, 'Iq')
def cleanup(self):
self.core.xmpp.remove_handler('Iq_show')

View File

@ -1,323 +0,0 @@
"""
Plugin destined to be used together with the Biboumi IRC gateway.
For more information about Biboumi, please see the `official website`_.
This plugin is here as a non-default extension of the poezio configuration
made to work with IRC rooms and logins. It also defines commands aimed at
reducing the amount of effort needed to navigate smoothly between IRC and
XMPP rooms.
Configuration
-------------
Every feature of this plugin is centered around its :ref:`configuration file <plugin-configuration>`,
so you have to make sure it is filled properly.
Global configuration
~~~~~~~~~~~~~~~~~~~~
.. glossary::
:sorted:
gateway
**Default:** ``irc.jabberfr.org``
The JID of the IRC gateway to use. If empty, irc.jabberfr.org will be
used. Please try to run your own, though, its painless to setup.
initial_connect
**Default:** ``true``
Set to ``true`` if you want to join all the rooms and try to
authenticate with nickserv when the plugin gets loaded. If it set to
``false``, you will have to use the :term:`/irc_login` command to
authenticate, and the :term:`/irc_join` command to join
preconfigured rooms.
.. note:: There is no nickname option because the default from poezio will be used.
Server-specific configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Write a configuration section for each server, with the server address as the
section name, and the following options:
.. glossary::
:sorted:
nickname
**Default:** ``[empty]``
Your nickname on this server. If empty, the default configuration will be used.
rooms [IRC plugin]
**Default:** ``[empty]``
The list of rooms to join on this server (e.g. ``#room1:#room2``).
.. note:: If no login_command or login_nick is set, the authentication phase
wont take place and you will join the rooms without authentication
with nickserv or whatever.
Commands
~~~~~~~~
.. glossary::
:sorted:
/irc_join
**Usage:** ``/irc_join <room or server>``
Join the specified room on the same server as the current tab (can
be a private conversation or a chatroom). If a server that appears
in the conversation is specified instead of a room, the plugin
will try to join all the rooms configured with autojoin on that
server.
/irc_query
**Usage:** ``/irc_query <nickname> [message]``
Open a private conversation with the given nickname, on the same IRC
server as the current tab (can be a private conversation or a
chatroom). Doing `/irc_query foo "hello there"` when the current
tab is #foo%irc.example.com@biboumi.example.com is equivalent to
``/message foo%irc.example.com@biboumi.example.com "hello there"``
Example configuration
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[irc]
gateway = irc.jabberfr.org
[irc.libera.chat]
nickname = mynick
login_nick = nickserv
login_command = identify mypassword
rooms = #testroom1:#testroom2
[irc.geeknode.org]
nickname = anothernick
login_nick = C
login_command = nick identify mypassword
rooms = #testvroum
.. _official website: http://biboumi.louiz.org/
"""
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 tabs
class Plugin(BasePlugin):
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,
usage='<room or server>',
help=('Join <room> in the same server as the '
'current tab (if it is an IRC tab). Or '
'join all the preconfigured rooms in '
'<server> '),
short='Join irc rooms more easily',
completion=self.completion_irc_join)
self.api.add_command(
'irc_query',
self.command_irc_query,
usage='<nickname> [message]',
help=('Open a private conversation with the '
'given <nickname>, on the current IRC '
'server. Optionally immediately send '
'the given message. For example, if the '
'current tab is #foo%irc.example.com@'
'biboumi.example.com, doing `/irc_query '
'nick "hi there"` is equivalent to '
'`/message nick%irc.example.com@biboumi.'
'example.com "hi there"`'),
short='Open a private conversation with an IRC user')
async def join(self, gateway: str, server: JID) -> None:
"Join irc rooms on a server"
nick: str = self.config.get_by_tabname(
'nickname', server, default='') or self.core.own_nick
rooms: List[str] = self.config.get_by_tabname(
'rooms', server, default='').split(':')
joins = []
for room in rooms:
room = '{}%{}@{}/{}'.format(room, server, gateway, nick)
joins.append(self.core.command.join(room))
await asyncio.gather(*joins)
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
for tab in self.core.tabs:
if tab.name.endswith(room_suffix) and tab.joined:
already_opened = True
break
if not already_opened:
await self.join(gateway, section)
@command_args_parser.quoted(1, 1)
async def command_irc_join(self, args: Optional[List[str]]) -> None:
"""
/irc_join <room or server>
"""
if not args:
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:
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:
await self.join_room(args[0])
@command_args_parser.quoted(1, 1)
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:
self.core.command.help('irc_query')
return
current_tab_info = self.get_current_tab_irc_info()
if not current_tab_info:
return
server, gateway = current_tab_info
nickname = args[0]
message = None
if len(args) == 2:
message = args[1]
jid = '{}%{}@{}'.format(nickname, server, gateway)
if message:
self.core.command.message('{} "{}"'.format(jid, message))
else:
self.core.command.message('{}'.format(jid))
async def join_server_rooms(self, section: JID) -> None:
"""
Join all the rooms configured for a section
(section = irc server)
"""
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:
nick = ''
suffix = '%{}@{}{}'.format(section, gateway, nick)
for room in rooms:
await self.core.command.join(room + suffix)
async def join_room(self, name: str) -> None:
"""
Join a room with only its name and the current tab
"""
current_tab_info = self.get_current_tab_irc_info()
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_jid.bare):
room += '/' + self.config.get_by_tabname('nickname', server_jid.bare)
await self.core.command.join(room)
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: str = self.config.getstr('gateway')
current = self.api.current_tab()
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')
return None
if isinstance(current, tabs.OneToOneTab):
if '%' not in current_jid.node:
server = current_jid.node
else:
ignored, server = current_jid.node.rsplit('%', 1)
elif isinstance(current, tabs.MucTab):
if '%' not in current_jid.node:
server = current_jid.node
else:
ignored, server = current_jid.node.rsplit('%', 1)
else:
self.api.information(
'The current tab does not appear to be an IRC one', 'Warning')
return None
return server, gateway
def completion_irc_join(self, the_input: Any) -> Completion:
"""
completion for /irc_join
"""
sections: List[str] = self.config.sections()
if 'irc' in sections:
sections.remove('irc')
return Completion(the_input.new_completion, sections, 1)

View File

@ -1,61 +0,0 @@
#! /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()

View File

@ -1,178 +0,0 @@
"""
Opens links in a browser.
Installation
------------
First use case: local use
~~~~~~~~~~~~~~~~~~~~~~~~~
If you use poezio on your workstation, this is for you.
You only have to load the plugin: ::
/load link
Second use case: remote use
~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you use poezio through SSH, this is for you.
.. note:: Small explanation: Poezio will create a `Unix FIFO`_ and send the commands in,
and you will have to run a dæmon locally with ssh, to get those commands.
First, set the :term:`exec_remote` option in the config file to ``true``. Then select
the directory you want to put the fifo in (default is the current
directory, :file:`./`), the :file:`poezio.fifo` file will be created there.
After that, load the plugin: ::
/load link
And open a link with :term:`/link` (as described below), this will create the FIFO.
You need to grab poezios sources on your client computer, or at least the `daemon.py`_
file.
Finally, on your client computer, run the ssh command:
.. code-block:: bash
ssh toto@example.org "cat ~/poezio/poezio.fifo" | python3 daemon.py
Usage
-----
.. glossary::
/link
**Usage:** ``/link [range] [command]``
This plugin adds a :term:`/link` command that will open the links in
``firefox``. If you want to use another browser, or any other
command, you can use the :term:`/set` command to change the
:term:`browser` option. You can also specify the command to execute
directly in the arguments. For example `/link "mpv %s"` will open
the first link found using mpv, instead of the configured browser.
:term:`/link` without argument will open the last link found
in the current tab, if any is found. An optional range
argument can be given, to select one or more links to
open. Examples:
``/link 1`` is equivalent to ``/link``
``/link 3`` will open the third link found in the current tab,
starting from the bottom.
``/link 1:5`` will open the last five links in the current tab
``/link :2`` will open the last two links
Options
-------
:term:`exec_remote`
To execute the command on your client
.. glossary::
browser
Set the default browser started by the plugin
.. _Unix FIFO: https://en.wikipedia.org/wiki/Named_pipe
.. _daemon.py: https://lab.louiz.org/poezio/poezio/raw/main/poezio/daemon.py
"""
import platform
import re
from poezio.plugin import BasePlugin
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+))|'
'(?:aesgcm://(?:\S+))|'
'(?:gopher://(?:\S+))|'
'(?:gemini://(?:\S+))'
'\b',
re.I | re.U
)
app_mapping = {
'Linux': 'xdg-open',
'Darwin': 'open',
}
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'link',
self.command_link,
usage='[num] [command]',
help=
'Opens the last link from the conversation into a browser.\n\
If [num] is given, then it will\open the num-th link displayed. \
Use a [command] argument to override the configured browser value.',
short='Open links into a browser')
def find_link(self, nb):
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in messages[::-1]:
matches = url_pattern.findall(clean_text(message.txt))
if matches:
for url in matches[::-1]:
if nb == 1:
return url
else:
nb -= 1
return None
def command_link(self, args):
args = common.shell_split(args)
start = 1
end = 1
# With two arguments, the first is the range, the second is the command
# With only one argument, it is a range if it starts with a number
# or :, otherwise it is a command
if len(args) == 2 or\
len(args) == 1 and (args[0][0].isnumeric() or args[0][0] == ":"):
if args[0].find(':') == -1:
try:
start = int(args[0])
end = start
except ValueError:
return self.api.run_command('/help link')
else:
start, end = args[0].split(':', 1)
if start == '':
start = 1
try:
start = int(start)
end = int(end)
except ValueError:
return self.api.information(
'Invalid range: %s' % (args[0]), 'Error')
command = None
if len(args) == 2:
command = args[1]
if len(args) == 1 and (not args[0][0].isnumeric()
and args[0][0] != ":"):
command = args[0]
for nb in range(start, end + 1):
link = self.find_link(nb)
if not link:
return self.api.information('No URL found.', 'Warning')
default = app_mapping.get(platform.system(), 'firefox')
if command is None:
self.core.exec_command(
[self.config.get('browser', default), link])
else:
self.core.exec_command([command, link])
def cleanup(self):
del self.config

View File

@ -1,93 +0,0 @@
"""
Marquee plugin: replicate the html <marquee/> tag with message corrections.
Usage of this plugin is not recommended.
Commands
--------
.. glossary::
/marquee <text>
Send the following text with <marquee/> behavior
Configuration
-------------
.. glossary::
:sorted:
refresh
**Default:** ``1``
Interval between each correction (the closest to 0 is the fastest)
total_duration
**Default:** ``30``
Total duration of the animation.
padding
**Default:** ``20``
Padding to use to move the text.
"""
import asyncio
from poezio.plugin import BasePlugin
from poezio import tabs
from poezio import xhtml
from poezio.decorators import command_args_parser
def move(text, step, spacing):
new_text = text + ("\u00A0" * spacing)
return new_text[-(step % len(new_text)):] + new_text[:-(
step % len(new_text))]
class Plugin(BasePlugin):
default_config = {
"marquee": {
"refresh": 1.0,
"total_duration": 30,
"padding": 20
}
}
def init(self):
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
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))
await tab.command_say(args)
is_muctab = isinstance(tab, tabs.MucTab)
msg_id = tab.last_sent_message["id"]
jid = tab.jid
event = self.api.create_delayed_event(
self.config.get("refresh"), self.delayed_event, jid, args, msg_id,
1, 0, is_muctab)
self.api.add_timed_event(event)
def delayed_event(self, jid, body, msg_id, step, duration, is_muctab):
if duration >= self.config.get("total_duration"):
return
message = self.core.xmpp.make_message(jid)
message["type"] = "groupchat" if is_muctab else "chat"
message["body"] = move(body, step, self.config.get("padding"))
message["replace"]["id"] = msg_id
message.send()
event = self.api.create_delayed_event(
self.config.get("refresh"), self.delayed_event, jid, body,
msg_id, step + 1, duration + self.config.get("refresh"),
is_muctab)
self.api.add_timed_event(event)

View File

@ -1,33 +0,0 @@
"""
Repeats the last message in the conversation.
Command
-------
.. glossary::
/mirror
**Usage:** ``/mirror``
"""
from poezio.plugin import BasePlugin
from poezio import tabs
class Plugin(BasePlugin):
def init(self):
for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'mirror',
handler=self.mirror,
help='Repeat the last message from the conversation.',
short='Repeat the last message from the conversation.')
def mirror(self, args):
messages = self.api.get_conversation_messages()
if not messages:
# Do nothing if the conversation doesnt contain any message
return
last_message = messages[-1]
self.api.send_message(last_message.txt)

View File

@ -1,98 +0,0 @@
"""
This plugin is here to send what you are listening to in a chat tab.
Installation
------------
You need `python-mpd`_, in its python3 version.
Then you can load the plugin.
.. code-block:: none
/load mpd_client
Configuration
-------------
You have to put the following into :file:`mpd_client.cfg`, as explained in
the :ref:`plugin-configuration` section.
.. note:: If you do not put anything, the plugin will try to connect to
:file:`localhost:6600` with no password.
.. code-block:: ini
[mpd_client]
host = the_mpd_host
port = 6600
password = password if necessary
Usage
-----
.. glossary::
/mpd
**Usage:** ``/mpd [full]``
The bare command will show the current song, artist, and album
``/mpd full`` will show the current song, artist, and album,
plus a nice progress bar in color.
.. _python-mpd: https://github.com/Mic92/python-mpd2
"""
from poezio.plugin import BasePlugin
from poezio.common import shell_split
from poezio.core.structs import Completion
from os.path import basename as base
from poezio import tabs
import mpd
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.MucTab, tabs.PrivateTab):
self.api.add_tab_command(
_class,
'mpd',
self.command_mpd,
usage='[full]',
help=
'Sends a message showing the current song of an MPD instance. If full is provided, the message is more verbose.',
short='Send the MPD status',
completion=self.completion_mpd)
def command_mpd(self, args):
args = shell_split(args)
c = mpd.MPDClient()
c.connect(
host=self.config.get('host', 'localhost'),
port=self.config.get('port', '6600'))
password = self.config.get('password', '')
if password:
c.password(password) #pylint: disable=no-member
current = c.currentsong() #pylint: disable=no-member
artist = current.get('artist', 'Unknown artist')
album = current.get('album', 'Unknown album')
title = current.get('title', base(
current.get('file', 'Unknown title')))
s = '%s - %s (%s)' % (artist, title, album)
if 'full' in args:
if 'elapsed' in current and 'time' in current:
current_time = float(c.status()['elapsed']) #pylint: disable=no-member
percents = int(current_time / float(current['time']) * 10)
s += ' \x192}[\x191}' + '-' * (
percents - 1) + '\x193}+' + '\x191}' + '-' * (
10 - percents - 1) + '\x192}]\x19o'
if not self.api.send_message('%s' % (s, )):
self.api.information('Cannot send result (%s)' % s, 'Error')
def completion_mpd(self, the_input):
return Completion(the_input.auto_completion, ['full'], quotify=False)

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
"""
This plugin adds a :term:`/pacokick` command, which is a random kick.
Usage
-----
.. glossary::
/pacokick
Run the command in a room where you are a moderator to
kick someone randomly.
"""
from random import choice
from poezio.tabs import MucTab
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'pacokick',
self.command_kick,
usage='',
help='Kick a random user.',
short='Kick a random user')
def command_kick(self, arg):
tab = self.api.current_tab()
if isinstance(tab, MucTab):
kickable = list(
filter(lambda x: x.affiliation in ('none', 'member'),
tab.users))
if kickable:
to_kick = choice(kickable)
if to_kick:
to_kick = to_kick.nick
tab.command_kick(to_kick + ' ' + arg)
else:
self.api.information('No one to kick :(', 'Info')

View File

@ -1,172 +0,0 @@
"""
This plugin allows you to ping an entity.
Command
-------
.. glossary::
/ping
**Usage (globally):** ``/ping <jid>``
**Usage (in a MUC tab):** ``/ping <jid or nick>``
**Usage (in a conversation tab):** ``/ping [jid]``
Globally, you can do ``/ping jid@example.com`` to get a ping.
In a MUC, you can either do it to a JID or a nick (``/ping nick`` or ``/ping
jid@example.com``).
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.contact import Contact, Resource
from poezio.core.structs import Completion
from poezio import tabs
import time
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'ping',
self.command_ping,
usage='<jid>',
help='Send an XMPP ping to jid (see XEP-0199).',
short='Send a ping',
completion=self.completion_ping)
self.api.add_tab_command(
tabs.MucTab,
'ping',
self.command_muc_ping,
usage='<jid|nick>',
help='Send an XMPP ping to jid or nick (see XEP-0199).',
short='Send a ping.',
completion=self.completion_muc_ping)
self.api.add_tab_command(
tabs.RosterInfoTab,
'ping',
self.command_roster_ping,
usage='<jid>',
help='Send an XMPP ping to jid (see XEP-0199).',
short='Send a ping.',
completion=self.completion_ping)
for _class in (tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'ping',
self.command_private_ping,
usage='[jid]',
help=
'Send an XMPP ping to the current interlocutor or the given JID.',
short='Send a ping',
completion=self.completion_ping)
@command_args_parser.raw
async def command_ping(self, arg):
if not arg:
return self.core.command.help('ping')
try:
jid = JID(arg)
except InvalidJID:
return self.api.information('Invalid JID: %s' % arg, 'Error')
start = time.time()
try:
iq = await self.core.xmpp.plugin['xep_0199'].send_ping(
jid=jid, timeout=10
)
delay = time.time() - start
error = False
reply = ''
if iq['type'] == 'error':
error_condition = iq['error']['condition']
reply = error_condition
# These IQ errors are not ping errors:
# 'service-unavailable': official "not supported" response as of RFC6120 (§8.4) and XEP-0199 (§4.1)
# 'feature-not-implemented': inoffcial not-supported response from many clients
if error_condition not in ('service-unavailable',
'feature-not-implemented'):
error = True
error_text = iq['error']['text']
if error_text:
reply = '%s: %s' % (error_condition, error_text)
if error:
message = '%s did not respond to ping: %s' % (jid, reply)
else:
reply = ' (%s)' % reply if reply else ''
message = '%s responded to ping after %ss%s' % (
jid, round(delay, 4), reply)
self.api.information(message, 'Info')
except IqTimeout:
self.api.information(
'%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]
l = self.resources()
users.extend(l)
return Completion(the_input.auto_completion, users, '', quotify=False)
@command_args_parser.raw
def command_private_ping(self, arg):
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):
if not arg:
return
user = self.api.current_tab().get_user_by_name(arg)
if user:
jid = self.api.current_tab().jid
jid.resource = user.nick
else:
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:
jid = arg
else:
current = self.api.current_tab().selected_row
if isinstance(current, Resource):
jid = current.jid
elif isinstance(current, Contact):
res = current.get_highest_priority_resource()
if res is not None:
jid =res.jid
asyncio.create_task(
self.command_ping(jid)
)
def resources(self):
l = []
for contact in roster.get_contacts():
for resource in contact.resources:
l.append(resource.jid)
return l
def completion_ping(self, the_input):
return Completion(
the_input.auto_completion, self.resources(), '', quotify=False)

View File

@ -1,68 +0,0 @@
"""
This plugins allows commands to be sent to poezio via a named pipe.
You can run the same commands that you would in the poezio input
(e.g. ``echo '/message toto@example.tld Hi' >> /tmp/poezio.fifo``).
Configuration
-------------
.. glossary::
:sorted:
pipename
**Default:** :file:`/tmp/poezio.fifo`
The path to the fifo which will receive commands.
"""
from poezio.plugin import BasePlugin
import os
import stat
import logging
import asyncio
log = logging.getLogger(__name__)
PIPENAME = "/tmp/poezio.fifo"
class Plugin(BasePlugin):
def init(self):
self.stop = False
self.pipename = self.config.get("pipename", PIPENAME)
if not os.path.exists(self.pipename):
os.mkfifo(self.pipename)
if not stat.S_ISFIFO(os.stat(self.pipename).st_mode):
raise TypeError("File %s is not a fifo file" % self.pipename)
self.fd = os.open(self.pipename, os.O_RDONLY | os.O_NONBLOCK)
self.data = b""
asyncio.get_event_loop().add_reader(self.fd, self.read_from_fifo)
def read_from_fifo(self):
data = os.read(self.fd, 512)
if not data:
# EOF, close the fifo. And reopen it
asyncio.get_event_loop().remove_reader(self.fd)
os.close(self.fd)
self.fd = os.open(self.pipename, os.O_RDONLY | os.O_NONBLOCK)
asyncio.get_event_loop().add_reader(self.fd, self.read_from_fifo)
self.data = b''
else:
self.data += data
l = self.data.split(b'\n', 1)
if len(l) == 2:
line, self.data = l
log.debug("run: %s" % (line.decode().strip()))
self.api.run_command(line.decode().strip())
def cleanup(self):
asyncio.get_event_loop().remove_reader(self.fd)
os.close(self.fd)

View File

@ -1,49 +0,0 @@
"""
This plugin adds a command (that can be bound to a key) that adds a random
number of dots in the input, making you look depressed, or overly thinking...
Installation
------------
Load the plugin.::
/load pointpoint
Then use the command: ::
/pointpoint
But since the goal is to be able to add the dots while typing a message,
entering a command is not really useful. To be useful, this plugin needs to
be used through a bound key, for example like this: ::
/bind M-. _exc_pointpoint
You just need to press Alt+. and this will insert dots in your message.
Command
-------
.. glossary::
/pointpoint
**Usage:** ``/pointpoint``
"""
from poezio.plugin import BasePlugin
from random import randrange
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'pointpoint',
self.command_pointpoint,
help='Insert a random number of dots in the input')
def command_pointpoint(self, args):
for i in range(randrange(8, 25)):
self.core.current_tab().input.do_command(".")

View File

@ -1,184 +0,0 @@
#!/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)

View File

@ -1,124 +0,0 @@
"""
This plugin allows you to quote messages easily.
Usage
-----
.. glossary::
/quote
**Usage:** ``/quote <message>``
The message must exist. You can use autocompletion to get the message
you want to quote easily.
Example:
.. code-block:: none
/quote "Pouet"
If the message "Pouet" exists, it will be put in the input. If not you
will get a warning.
Options
-------
.. glossary::
:sorted:
before_quote
**Default value:** ``[empty]``
Text to insert before the quote. ``%(nick)s`` and ``%(time)s`` can
be used to insert the nick of the user who sent the message or the
time of the message.
after_quote
**Default value:** ``[empty]``
Text to insert after the quote. ``%(nick)s`` and ``%(time)s`` can
be used to insert the nick of the user who sent the message or the
time of the message.
"""
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
import logging
log = logging.getLogger(__name__)
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.MucTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.PrivateTab):
self.api.add_tab_command(
_class,
'quote',
self.command_quote,
usage='<message>',
help='Quote the message you typed if it exists.',
short='Quote a message.',
completion=self.completion_quote)
def command_quote(self, args):
args = common.shell_split(args)
if len(args) == 1:
message = args[-1]
else:
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': str_time,
}
after = self.config.get('after_quote', '') % {
'nick': message.nickname or '',
'time': str_time,
}
self.core.insert_input_text(
'%(before)s%(quote)s%(after)s' % {
'before': before.replace('\\n', '\n').replace('[SP]', ' '),
'quote': clean_text(message.txt),
'after': after.replace('\\n', '\n').replace('[SP]', ' ')
})
else:
self.api.information('No message found', 'Warning')
def find_message(self, txt):
messages = self.api.get_conversation_messages()
if not messages:
return None
for message in messages[::-1]:
if isinstance(message, Message) and clean_text(message.txt) == txt:
return message
return None
def completion_quote(self, the_input):
def message_match(msg):
return input_message.lower() in clean_text(msg.txt).lower()
messages = self.api.get_conversation_messages()
if not messages:
return
text = the_input.get_text()
args = common.shell_split(text)
if not text.endswith(' '):
input_message = args[-1]
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] if isinstance(msg, Message)],
''
)

View File

@ -1,47 +0,0 @@
"""
This plugin colors each character of a message with a random color.
.. note:: As ticket `#3273`_ puts it, the final output is closer to vomit than a rainbow.
Usage
-----
.. glossary::
/rainbow
Say something in a Chat tab.
.. note:: Can create fun things when used with :ref:`The figlet plugin <figlet-plugin>`.
.. _#3273: https://lab.louiz.org/poezio/poezio/-/issues/3273
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
import random
possible_colors = list(range(256))
# remove the colors that are almost white or almost black
for col in [
16, 232, 233, 234, 235, 236, 237, 15, 231, 255, 254, 253, 252, 251
]:
possible_colors.remove(col)
def rand_color():
return '\x19%s}' % (random.choice(possible_colors), )
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.rainbowize)
self.api.add_event_handler('private_say', self.rainbowize)
self.api.add_event_handler('conversation_say', self.rainbowize)
def rainbowize(self, msg, tab):
msg['body'] = ''.join([
'%s%s' % (
rand_color(),
char,
) for char in xhtml.clean_text(msg['body'])
])

View File

@ -1,41 +0,0 @@
"""
This plugin makes you have a random nick when joining a chatroom.
Usage
-----
To have a random nick, just join a room with RANDOM as your nick. It will
automatically be changed to something random, for example: ::
/join coucou@conference.example.com/RANDOM
"""
from poezio.plugin import BasePlugin
from random import choice
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('joining_muc', self.change_nick_to_random)
self.api.add_event_handler('changing_nick', self.change_nick_to_random)
def change_nick_to_random(self, presence):
to = presence["to"]
if to.resource == 'RANDOM':
to.resource = gen_nick(3)
presence["to"] = to
s = ["i", "ou", "ou", "on", "a", "o", "u", "i"]
c = [
"b", "c", "d", "f", "g", "h", "j", "k", "m", "l", "n", "p", "r", "s", "t",
"v", "z"
]
def gen_nick(size):
res = ''
for _ in range(size):
res += '%s%s' % (choice(c), choice(s))
return res

View File

@ -1,75 +0,0 @@
"""
This plugins adds a :term:`/rkick` and a :term:`/rban` command,
in order to kick/ban according to a regex on a nick.
Commands
--------
Those commands take a regular expression (as defined in the
`re module documentation`_) as a parameter.
.. glossary::
:sorted:
/rkick
**Usage:** ``/rkick <regex>``
Kick a participant using a regex.
/rban
**Usage:** ``/rban <regex>``
Ban a participant using a regex.
.. _re module documentation: http://docs.python.org/3/library/re.html
"""
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
import re
class Plugin(BasePlugin):
def init(self):
self.api.add_tab_command(
MucTab,
'rkick',
self.command_rkick,
usage='<regex>',
help='Kick occupants of a room according to a regex',
short='Regex Kick')
self.api.add_tab_command(
MucTab,
'rban',
self.command_rban,
usage='<regex>',
help='Ban occupants of a room according to a regex',
short='Regex Ban')
def return_users(self, users, regex):
try:
reg = re.compile(regex)
except:
return []
ret = []
for user in users:
if reg.match(user.nick):
ret.append(user)
return ret
def command_rban(self, regex):
tab = self.api.current_tab()
users = self.return_users(tab.users, regex)
for user in users:
tab.command_ban(user.nick)
def command_rkick(self, regex):
tab = self.api.current_tab()
users = self.return_users(tab.users, regex)
for user in users:
tab.command_kick(user.nick)

View File

@ -1,168 +0,0 @@
"""
Usage
-----
This plugin defines three new global commands: :term:`/remind`,
:term:`/done`, and :term:`/tasks`.
.. glossary::
/remind
**Usage:** ``/remind <time> <todo>``
This command will remind you to do ``todo`` every ``time``.
/done
**Usage:** ``/done <id>``
Remove a reminder.
The ``id`` is found using :term:`/tasks`.
/tasks
Print a list of the tasks, their ids, and their frequency, into the
information buffer.
Time format
-----------
In seconds:
.. code-block:: none
/remind 600 Work!
Will remind you to work every 10 minutes.
Defining the time in seconds is not really practical, so you can describe it
with days, hours, and minutes, in a time-string, e.g:
.. code-block:: python
/remind 1h23m "Get up"
Will remind you to get up every 1 hour 23 minutes.
"""
from poezio.core.structs import Completion
from poezio.plugin import BasePlugin
from poezio import timed_events
from poezio import common
import curses
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'remind',
self.command_remind,
usage='<seconds> <todo>',
help='Remind you of <todo> every <time> seconds.',
short='Remind you of a task',
completion=self.completion_remind)
self.api.add_command(
'done',
self.command_done,
usage='<id>',
help='Stop reminding you do the task identified by <id>.',
short='Remove a task',
completion=self.completion_done)
self.api.add_command(
'tasks',
self.command_tasks,
usage='',
help='List all the current tasks and their ids.',
short='List current tasks')
self.tasks = {}
self.count = 0
for option in self.config.options(self.__module__):
id, secs = option.split(',')
id = int(id)
if id > self.count:
self.count = id
value = self.config.get(option, '')
self.tasks[id] = (int(secs), value)
self.config.remove_section(self.__module__)
self.config.add_section(self.__module__)
if self.tasks:
self.count += 1
self.command_tasks('', nocommand=True)
def command_remind(self, arg):
args = common.shell_split(arg)
if len(args) < 2:
return
time = common.parse_str_to_secs(args[0])
if not time:
return
self.tasks[self.count] = (time, args[1])
timed_event = timed_events.DelayedEvent(time, self.remind, self.count)
self.api.add_timed_event(timed_event)
self.api.information(
'Task %s added: %s every %s.' % (self.count, args[1],
common.parse_secs_to_str(time)),
'Info')
self.count += 1
def completion_remind(self, the_input):
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
return Completion(the_input.auto_completion,
["60", "5m", "15m", "30m", "1h", "10h", "1d"],
'')
def completion_done(self, the_input):
return Completion(the_input.auto_completion,
["%s" % key for key in self.tasks], '')
def command_done(self, arg="0"):
try:
id_ = int(arg)
except ValueError:
return
if id_ not in self.tasks:
return
self.api.information('Task %s: %s [DONE]' % (id_, self.tasks[id_][1]),
'Info')
del self.tasks[id_]
def command_tasks(self, arg, nocommand=None):
if nocommand:
s = 'The following tasks were loaded:\n'
else:
s = 'The following tasks are active:\n'
for key in self.tasks:
s += 'Task %s: %s every %s.\n' % (key, repr(self.tasks[key][1]),
common.parse_secs_to_str(
self.tasks[key][0]))
if s:
self.api.information(s, 'Info')
def remind(self, id_=0):
if id_ not in self.tasks:
return
self.api.information('Task %s: %s' % (id_, self.tasks[id_][1]), 'Info')
if self.config.get('beep', '') == 'true':
curses.beep()
timed_event = timed_events.DelayedEvent(self.tasks[id_][0],
self.remind, id_)
self.api.add_timed_event(timed_event)
def cleanup(self):
if self.tasks:
self.config.remove_section(self.__module__)
self.config.add_section(self.__module__)
for task in self.tasks:
self.config.set('%s,%s' % (task, self.tasks[task][0]),
self.tasks[task][1])
self.config.write()

View File

@ -1,24 +0,0 @@
"""
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'])

View File

@ -1,203 +0,0 @@
"""
``reorder`` plugin: Reorder the tabs according to a layout
Commands
--------
.. glossary::
/reorder
**Usage:** ``/reorder``
Reorder the tabs according to the configuration.
/save_order
**Usage:** ``/save_order``
Save the current tab order to the configuration.
Configuration
-------------
The configuration file must contain a section ``[reorder]`` and each option
must be formatted like ``[tab number] = [tab type]:[tab name]``.
For example:
.. code-block:: ini
[reorder]
1 = muc:toto@conference.example.com
2 = muc:example@muc.example.im
3 = dynamic:robert@example.org
The ``[tab number]`` must be at least ``1``; if the range is not entirely
covered, e.g.:
.. code-block:: ini
[reorder]
1 = muc:toto@conference.example.com
3 = dynamic:robert@example.org
Poezio will insert gaps between the tabs in order to keep the specified
numbering (so in this case, there will be a tab 1, a tab 3, but no tab 2).
The ``[tab type]`` must be one of:
- ``muc`` (for multi-user chats)
- ``private`` (for chats with a specific user inside a multi-user chat)
- ``dynamic`` (for normal, dynamic conversations tabs)
- ``static`` (for conversations with a specific resource)
And finally, the ``[tab name]`` must be:
- For a type ``muc``, the bare JID of the room
- For a type ``private``, the full JID of the user (room JID with the username as a resource)
- For a type ``dynamic``, the bare JID of the contact
- 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
from poezio.config import config
TEXT_TO_TAB = {
'muc': tabs.MucTab,
'private': tabs.PrivateTab,
'dynamic': tabs.DynamicConversationTab,
'static': tabs.StaticConversationTab,
'empty': tabs.GapTab
}
TAB_TO_TEXT = {
tabs.MucTab: 'muc',
tabs.DynamicConversationTab: 'dynamic',
tabs.PrivateTab: 'private',
tabs.StaticConversationTab: 'static',
tabs.GapTab: 'empty'
}
def parse_config(tab_config):
result = {}
for option in tab_config.options('reorder'):
if not option.isdecimal():
continue
pos = int(option)
if pos in result or pos <= 0:
return None
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)
return result
def check_tab(tab):
for cls, rep in TAB_TO_TEXT.items():
if isinstance(tab, cls):
return rep
return ''
def parse_runtime_tablist(tablist):
props = []
i = 0
for tab in tablist[1:]:
i += 1
result = check_tab(tab)
# Don't serialize gap tabs as they're recreated automatically
if result != 'empty' and isinstance(tab, 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',
self.command_reorder,
help='Reorder all tabs using the pre-defined'
' layout from the configuration file.')
self.api.add_command(
'save_order',
self.command_save_order,
help='Save the current tab layout')
@command_args_parser.ignored
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) -> None:
"""
/reorder
"""
tabs_spec = parse_config(self.config)
if not tabs_spec:
self.api.information('Invalid reorder config', 'Error')
return None
old_tabs = self.core.tabs.get_tabs()
roster = old_tabs.pop(0)
create_gaps = config.get('create_gaps')
new_tabs = [roster]
last = 0
for pos in sorted(tabs_spec):
if create_gaps and pos > last + 1:
new_tabs += [
tabs.GapTab() for i in range(pos - last - 1)
]
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())
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

View File

@ -1,113 +0,0 @@
"""
Replace some patterns in a message before sending it.
Usage
-----
Insert a pattern in the form
.. code-block:: none
%pattern%
in your message, and it will be replaced by the corresponding text.
The list of provided patterns is:
- **time**: Insert the current time
- **date**: Insert the current date
- **datetime**: Insert the current date and time
- **random_nick**: Insert a random nick from the current MUC
- **dice**: Insert a random number between 1 and 6
Add your own pattern
--------------------
You can easily edit this plugin to add your own patterns. For example if
dont want to search for an insult every time youre angry, you can create a
curse pattern this way:
- In the init(self) method of the Plugin class, add something like
.. code-block:: python
self.patterns['curse'] = replace_curse
- then define a function (not a method of the Plugin class) at the bottom
of the file. For example:
.. code-block:: python
def replace_curse(message, tab):
return random.choice(['dumb shit', 'idiot', 'moron'])
and you can now use something like
.. code-block:: none
Shut up, %curse%!
in your everyday-conversations.
For more convenience, you can read your nice words from a file, do whatever
you want in that function, as long as it returns a string.
"""
from poezio.plugin import BasePlugin
from poezio import tabs
import datetime
import random
import re
from slixmpp.xmlstream.stanzabase import JID
class Plugin(BasePlugin):
def init(self):
self.patterns = {}
self.api.add_event_handler('conversation_say', self.replace_pattern)
self.api.add_event_handler('muc_say', self.replace_pattern)
self.api.add_event_handler('private_say', self.replace_pattern)
self.patterns['time'] = replace_time
self.patterns['date'] = replace_date
self.patterns['datetime'] = replace_datetime
self.patterns['random_nick'] = replace_random_user
self.patterns['dice'] = replace_dice
def replace_pattern(self, message, tab):
"""
Look for a %*% pattern in the message and replace it by the result
of the corresponding function.
"""
body = message['body']
for pattern in self.patterns:
new = body
body = re.sub('%%%s%%' % pattern,
lambda x: self.patterns[pattern](message, tab), body)
message['body'] = body
def replace_time(message, tab):
return datetime.datetime.now().strftime("%X")
def replace_date(message, tab):
return datetime.datetime.now().strftime("%Y-%m-%d")
def replace_datetime(message, tab):
return datetime.datetime.now().strftime("%c")
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([tab.jid.resource, tab.own_nick])
else:
# that doesnt make any sense. By why use this pattern in a
# ConversationTab anyway?
return tab.jid.full
def replace_dice(message, tab):
return str(random.randrange(1, 7))

View File

@ -1,37 +0,0 @@
"""
Replace some word with some other word in a message before sending it.
Configuration example
---------------------
.. code-block:: ini
[replace_word]
# How to appear casual in your daily conversations.
yes = yep
no = nope
Usage
-----
Just use the word in a message. It will be replaced automatically.
"""
from poezio.plugin import BasePlugin
import re
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('conversation_say', self.replace_pattern)
self.api.add_event_handler('muc_say', self.replace_pattern)
self.api.add_event_handler('private_say', self.replace_pattern)
def replace_pattern(self, message, tab):
"""
Look for a given word in the message and replace it by the corresponding word.
"""
body = message['body']
for before in self.config.options("replace_word"):
after = self.config.get(before, before)
body = re.sub(r"\b%s\b" % before, after, body)
message['body'] = body

View File

@ -1,16 +0,0 @@
"""
Reverse everything you say (``Je proteste énergiquement`` will become
``tnemeuqigrené etsetorp eJ``)
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.revstr)
self.api.add_event_handler('conversation_say', self.revstr)
self.api.add_event_handler('private_say', self.revstr)
def revstr(self, msg, tab):
msg['body'] = xhtml.clean_text(msg['body'])[::-1]

View File

@ -1,15 +0,0 @@
"""
Once loaded, every line of your messages will be stripped of their trailing spaces.
"""
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.rstrip)
self.api.add_event_handler('conversation_say', self.rstrip)
self.api.add_event_handler('private_say', self.rstrip)
def rstrip(self, msg, tab):
msg['body'] = '\n'.join(
line.rstrip() for line in msg['body'].split('\n'))

View File

@ -1,119 +0,0 @@
"""
This plugin will set your status to **away** if you detach your screen.
The default behaviour is to check for both tmux and screen (in that order).
Configuration options
---------------------
.. glossary::
use_screen
**Default:** ``true``
Try to find an attached screen.
use_tmux
**Default:** ``true``
Try to find and attached tmux.
use_csi
**Default:** ``false``
Use `client state indication`_ to limit bandwidth (thus CPU) usage when detached. WARNING: using CSI together with chatrooms will result in inaccurate logs due to presence filtering or other inaccuracies.
.. _client state indication: https://xmpp.org/extensions/xep-0352.html
"""
from poezio.plugin import BasePlugin
import os
import stat
import pyinotify
import asyncio
DEFAULT_CONFIG = {
'screen_detach': {
'use_tmux': True,
'use_screen': True,
'use_csi': False
}
}
# overload if this is not how your stuff
# is configured
try:
LOGIN = os.getlogin() or ''
LOGIN_TMUX = os.getuid()
except Exception:
LOGIN = os.getenv('USER') or ''
LOGIN_TMUX = os.getuid()
SCREEN_DIR = '/var/run/screens/S-%s' % LOGIN
TMUX_DIR = '/tmp/tmux-%s' % LOGIN_TMUX
def find_screen(path):
if not os.path.isdir(path):
return
for f in os.listdir(path):
path = os.path.join(path, f)
if screen_attached(path):
return path
def screen_attached(socket):
return (os.stat(socket).st_mode & stat.S_IXUSR) != 0
class Plugin(BasePlugin, pyinotify.Notifier):
default_config = DEFAULT_CONFIG
def init(self):
sock_path = None
if self.config.get('use_tmux'):
sock_path = find_screen(TMUX_DIR)
if sock_path is None and self.config.get('use_screen'):
sock_path = find_screen(SCREEN_DIR)
# Only actually do something if we found an attached screen (assuming only one)
if sock_path:
self.attached = True
wm = pyinotify.WatchManager()
wm.add_watch(sock_path,
pyinotify.EventsCodes.ALL_FLAGS['IN_ATTRIB'])
pyinotify.Notifier.__init__(
self, wm, default_proc_fun=HandleScreen(plugin=self))
asyncio.get_event_loop().add_reader(self._fd, self.process)
else:
self.api.information(
'screen_detach plugin: No tmux or screen found', 'Warning')
self.attached = False
def process(self):
self.read_events()
self.process_events()
def cleanup(self):
asyncio.get_event_loop().remove_reader(self._fd)
def update_screen_state(self, socket):
attached = screen_attached(socket)
if attached != self.attached:
self.attached = attached
status = 'available' if self.attached else 'away'
self.core.command.status(status)
if self.config.get('use_csi'):
if self.attached:
self.core.xmpp.plugin['xep_0352'].send_active()
else:
self.core.xmpp.plugin['xep_0352'].send_inactive()
class HandleScreen(pyinotify.ProcessEvent):
def my_init(self, **kwargs):
self.plugin = kwargs['plugin']
def process_IN_ATTRIB(self, event):
self.plugin.update_screen_state(event.path)

View File

@ -1,80 +0,0 @@
"""
Send a message after a certain delay.
Usage
-----
This plugin adds a command to the chat tabs.
.. glossary::
/send_delayed
**Usage:** ``/send_delayed <delay> <message>``
Send a message after a given delay to the current tab.
The delay can be either in seconds or in a classic XdXhXm format
(e.g. ``7h3m`` or ``1d``), some examples are given with the
autocompletion.
"""
import asyncio
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
from poezio import tabs
from poezio import common
from poezio import timed_events
class Plugin(BasePlugin):
def init(self):
for _class in (tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab, tabs.MucTab):
self.api.add_tab_command(
_class,
'send_delayed',
self.command_delayed,
usage='<delay> <message>',
help='Send <message> with a delay of <delay> seconds.',
short='Send a message later',
completion=self.completion_delay)
@command_args_parser.quoted(2)
def command_delayed(self, args):
if args is None:
self.core.command.help('send_delayed')
return
delay_str, txt = args
delay = common.parse_str_to_secs(delay_str)
if not delay:
self.api.information('Failed to parse %s.' % delay_str, 'Error')
return
tab = self.api.current_tab()
timed_event = timed_events.DelayedEvent(delay, self.say, (tab, txt))
self.api.add_timed_event(timed_event)
self.api.information(
'Delayed message will be sent in %ds (%s).' % (delay, delay_str),
'Info')
def completion_delay(self, the_input):
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
return Completion(the_input.auto_completion,
["60", "5m", "15m", "30m", "1h", "10h", "1d"],
'')
def say(self, args=None):
if not args:
return
tab = args[0]
# anything could happen to the tab during the interval
try:
asyncio.ensure_future(tab.command_say(args[1]))
except:
pass

View File

@ -1,67 +0,0 @@
"""
This plugin adds a ``/server_part`` command to leave all rooms
on a server.
Command
-------
.. glossary::
/server_part
**Usage:** ``/server_part [<server> [message]]``
Leave all rooms on ``<server>``, if not provided and the current
tab is a chatroom tab, it will leave all rooms on the current server.
``[message]`` can indicate a quit message.
"""
from slixmpp import JID, InvalidJID
from poezio.plugin import BasePlugin
from poezio.tabs import MucTab
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
class Plugin(BasePlugin):
def init(self):
self.api.add_command(
'server_part',
self.command_server_part,
usage='[<server> [message]]',
short='Leave all the rooms on a server',
help='Leave all the rooms on a sever.',
completion=self.completion_server_part)
@command_args_parser.quoted(0, 2, defaults=[])
def command_server_part(self, args):
current_tab = self.api.current_tab()
if not args and not isinstance(current_tab, MucTab):
return self.core.command_help('server_part')
elif not args:
jid = current_tab.jid.bare
message = None
elif len(args) == 1:
try:
jid = JID(args[0]).domain
except InvalidJID:
return self.core.command_help('server_part')
message = None
else:
try:
jid = JID(args[0]).domain
except InvalidJID:
return self.core.command_help('server_part')
message = args[1]
for tab in self.core.get_tabs(MucTab):
if tab.name.endswith(jid):
tab.command_part(message)
def completion_server_part(self, the_input):
serv_list = set()
for tab in self.core.get_tabs(MucTab):
if tab.joined:
serv = tab.jid.server
serv_list.add(serv)
return Completion(the_input.new_completion, sorted(serv_list), 1, ' ')

View File

@ -1,17 +0,0 @@
"""
Shuffle the words in every message you send in a :ref:`muctab`
(may/should confuse the reader).
"""
from poezio.plugin import BasePlugin
from random import shuffle
from poezio import xhtml
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.shuffle)
def shuffle(self, msg, tab):
split = xhtml.clean_text(msg['body']).split()
shuffle(split)
msg['body'] = ' '.join(split)

View File

@ -1,165 +0,0 @@
"""
This plugin lets you execute a command, to notify you from new important
messages.
Installation and configuration
------------------------------
You need to create a plugin configuration file. Create a file named :file:`simple_notify.cfg`
into your plugins configuration directory (:file:`~/.config/poezio/plugins` by
default), and fill it like this:
First example:
.. code-block:: ini
[simple_notify]
command = notify-send -i /path/to/poezio/data/poezio_80.png "New message from %(from)s" "%(body)s"
Second example:
.. code-block:: ini
[simple_notify]
command = echo \\<%(from)s\\> %(body)s >> some.fifo
delay = 3
after_command = echo >> some.fifo
You can put any command, instead of these ones. You can also use the
special keywords ``%(from)s`` and ``%(body)s`` that will be replaced
directly in the command line by the author of the message, and the body.
The first example shown above will display something like this:
.. figure:: ../images/simple_notify_example.png
:alt: Simple notify example
The second example will first write the author and the message in a
fifo, that fifo can locally be read by some other program (was tested
with the xmobar PipeReader command, which displays what is read from a
fifo into a status bar. Be careful, you have two different fifos in
that case, dont get confused). The :term:`delay` and :term:`after_command` options
are used to erase/delete/kill the notification after a certain
delay. In our example it is used to display an empty message in our
xmobar, erasing the notification after 3 seconds.
Third example:
.. code-block:: ini
[simple_notify]
command = notify-send -i /path/to/poezio/data/poezio_80.png "New message from %(from)s" "%(body)s"
muc_too = true
muc_list = someroom@conference.jabber.org:someotherroom@conference.jabber.org
If present and set to ``True``, the ``muc_too`` option will also trigger a
notification when a new message arrives on a Multi User Chat you've joined.
If present and set to a colon separated list of muc JIDs, muc_list together
with muc_too = true will only notify when a new message arrives on a Multi
User Chat, you've joined if it is present on the list.
.. note:: If you set the :term:`exec_remote` option to ``true`` into the
main configuration file, the command will be executed remotely
(as explained in the :ref:`link-plugin` plugin help).
Options defined
---------------
.. glossary::
:sorted:
command
The command to execute (with special keywords ``%{from}s`` and ``${body}s``)
delay
Delay after which :term:`after_command` must be executed.
after_command
Command to run after :term:`delay`. You probably want to clean up things.
muc_too
Boolean indicating whether new messages in Multi User Chat rooms should
trigger a notification or not.
"""
from poezio.plugin import BasePlugin
from poezio.xhtml import get_body_from_message_stanza
from poezio.timed_events import DelayedEvent
import shlex
from poezio import common
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('private_msg', self.on_private_msg)
self.api.add_event_handler('conversation_msg',
self.on_conversation_msg)
if self.config.get('muc_too', False):
self.api.add_event_handler('muc_msg', self.on_muc_msg)
self.api.add_event_handler('highlight', self.on_highlight)
def on_private_msg(self, message, tab):
fro = message['from']
self.do_notify(message, fro)
def on_highlight(self, message, tab):
whitelist = self.config.get('muc_list', '').split(':')
# prevents double notifications
if message['from'].bare in whitelist:
return
fro = message['from'].resource
self.do_notify(message, fro)
def on_conversation_msg(self, message, tab):
fro = message['from'].bare
if fro.bare != self.core.xmpp.boundjid.bare:
self.do_notify(message, fro)
def on_muc_msg(self, message, tab):
# Don't notify if message is from yourself
if message['from'].resource == tab.own_nick:
return
fro = message['from'].full
muc = message['from'].bare
whitelist = self.config.get('muc_list', '').split(':')
# Prevent old messages to be notified
# find_delayed_tag(message) returns (True, the datetime) or
# (False, None)
if not common.find_delayed_tag(message)[0]:
# Only notify if whitelist is empty or muc in whitelist
if whitelist == [''] or muc in whitelist:
self.do_notify(message, fro)
def do_notify(self, message, fro):
body = get_body_from_message_stanza(message, use_xhtml=False)
if not body:
return
command_str = self.config.get('command', '').strip()
if not command_str:
self.api.information(
'No notification command was provided in the configuration file',
'Warning')
return
command = [
arg % {
'body': body.replace('\n', ' '),
'from': fro
} for arg in shlex.split(command_str)
]
self.core.exec_command(command)
after_command_str = self.config.get('after_command', '').strip()
if not after_command_str:
return
after_command = [
arg % {
'body': body.replace('\n', ' '),
'from': fro
} for arg in shlex.split(after_command_str)
]
delayed_event = DelayedEvent(
self.config.get('delay', 1), self.core.exec_command, after_command)
self.api.add_timed_event(delayed_event)

View File

@ -1,16 +0,0 @@
"""
Insert a space between each character, in messages that you send, making
them horrible to read.
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.add_spaces)
self.api.add_event_handler('conversation_say', self.add_spaces)
self.api.add_event_handler('private_say', self.add_spaces)
def add_spaces(self, msg, tab):
msg['body'] = " ".join(x for x in xhtml.clean_text(msg['body']))

View File

@ -1,25 +0,0 @@
"""
Add a subtle little advertising in your messages.
Configuration
-------------
.. code-block:: ini
[spam]
ad = Im a happy poezio user. Get it at http://poezio.eu
"""
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.advert)
self.api.add_event_handler('conversation_say', self.advert)
self.api.add_event_handler('private_say', self.advert)
def advert(self, msg, tab):
msg['body'] = "%s\n\n%s" % (msg['body'],
self.config.get("ad", "Sent from poezio"))

View File

@ -1,43 +0,0 @@
"""
This plugin adds several aliases, to shorten status changes.
Aliases
-------
.. glossary::
:sorted:
/afk
/away
Set your status to ``away``
/dnd
/busy
Set your status to ``dnd``
/available
Set your status to ``available``
/chat
Set your status to ``chat``
/xa
Set your status to ``xa``
"""
from poezio.plugin import BasePlugin
class Plugin(BasePlugin):
"""
Adds several convenient aliases to /status command
"""
def init(self):
for st in ('dnd', 'busy', 'afk', 'chat', 'xa', 'away', 'available'):
self.api.add_command(
st,
lambda line, st=st: self.api.run_command('/status ' + st + ' "' + line + '"'),
usage='[status message]',
short='Set your status as %s' % st,
help='Set your status as %s' % st)

View File

@ -1,97 +0,0 @@
'''
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)

View File

@ -1,60 +0,0 @@
"""
Repeats the last word of the last message in the conversation, and use it in
an annoying Cest toi le sentence.
Installation
------------
You only have to load the plugin:
.. code-block:: none
/load stoi
.. glossary::
/stoi
**Usage:** ``/stoi``
"""
from poezio.plugin import BasePlugin
from poezio import tabs
import string
from poezio import xhtml
import random
char_we_dont_want = string.punctuation + ' ’„“”…«»'
class Plugin(BasePlugin):
def init(self):
for tab_type in (tabs.MucTab, tabs.PrivateTab, tabs.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
tab_type,
'stoi',
handler=self.stoi,
help="Repeats the last word of the last "
"message in the conversation, and "
"use it in an annoying “Cest toi "
"le” sentence.",
short='Cest toi le stoi.')
def stoi(self, args):
messages = self.api.get_conversation_messages()
if not messages:
# Do nothing if the conversation doesnt contain any message
return
last_message = messages[-1]
txt = xhtml.clean_text(last_message.txt)
for char in char_we_dont_want:
txt = txt.replace(char, ' ')
if txt.strip():
last_word = txt.split()[-1]
else:
last_word = "vide"
intro = "C'est toi " if random.getrandbits(1) else "Stoi "
if last_word[0] in 'aeiouAEIOUÀàÉéÈè':
msg = intro + ('l%s' % last_word)
else:
msg = intro + ('le %s' % last_word)
self.api.send_message(msg)

View File

@ -1,117 +0,0 @@
"""
The command added by this plugin sends a message to someone when he next joins.
Usage
-----
This plugin defines two new commands for chatroom tabs:
:term:`/tell`, :term:`/untell`, and :term:`/list_tell`.
.. glossary::
:sorted:
/tell
**Usage:** ``/tell <nick> <message>``
Send *message* to *nick* at his next join.
/untell
**Usage:** ``/untell <nick>``
Cancel all scheduled messages to *nick*.
/list_tell
**Usage:** ``/list_tell``
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
from poezio import tabs
class Plugin(BasePlugin):
def init(self):
self.api.add_tab_command(
tabs.MucTab,
'tell',
self.command_tell,
usage='<nick> <message>',
help='Will tell <nick> of <message> when he next joins.',
short='Send a message when someone joins')
self.api.add_tab_command(
tabs.MucTab,
'untell',
self.command_untell,
usage='<nick>',
help='Remove the planned messages from /tell.',
short='Cancel a /tell message',
completion=self.completion_untell)
self.api.add_tab_command(
tabs.MucTab,
'list_tell',
self.command_list_tell,
usage='',
help='List currently queued messages')
self.api.add_event_handler('muc_join', self.on_join)
self.api.add_event_handler('muc_nickchange', self.on_join)
# {tab -> {nick -> [messages]}
self.tabs = {}
def on_join(self, presence, tab):
if tab not in self.tabs:
return
nick = presence['from'].resource
if nick not in self.tabs[tab]:
return
for i in self.tabs[tab][nick]:
asyncio.ensure_future(tab.command_say("%s: %s" % (nick, i)))
del self.tabs[tab][nick]
@command_args_parser.ignored
def command_list_tell(self):
tab = self.api.current_tab()
if not self.tabs.get(tab):
self.api.information('No message queued.', 'Info')
return
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:
build.append(' - %s' % message)
self.api.information('\n'.join(build), 'Info')
@command_args_parser.quoted(2)
def command_tell(self, args):
"""/tell <nick> <message>"""
if args is None:
self.core.command.help('tell')
return
nick, msg = args
tab = self.api.current_tab()
if tab not in self.tabs:
self.tabs[tab] = {}
if nick not in self.tabs[tab]:
self.tabs[tab][nick] = []
self.tabs[tab][nick].append(msg)
self.api.information('Message for %s queued' % nick, 'Info')
def command_untell(self, args):
"""/untell <nick>"""
tab = self.api.current_tab()
if tab not in self.tabs:
return
nick = args
if nick not in self.tabs[tab]:
return
del self.tabs[tab][nick]
self.api.information('Messages for %s unqueued' % nick, 'Info')
def completion_untell(self, the_input):
tab = self.api.current_tab()
if tab not in self.tabs:
return Completion(the_input.auto_completion, [], '')
return Completion(
the_input.auto_completion, list(self.tabs[tab]), '', quotify=False)

View File

@ -1,26 +0,0 @@
from poezio.plugin import BasePlugin
from poezio import tabs
class Plugin(BasePlugin):
def init(self):
self.api.add_command('plugintest', self.command_plugintest,
'Test command')
self.api.add_tab_command(tabs.MucTab, 'plugintest',
self.command_tab_plugintest, 'Test command')
self.api.add_slix_event_handler('message', self.on_message)
self.api.information("Plugin loaded")
def cleanup(self):
self.api.information("Plugin unloaded")
def on_message(self, message):
self.api.information(
"Test plugin received message: {}".format(message))
def command_tab_plugintest(self, args):
self.api.information("Command for MucTabs! With args {}".format(args))
self.api.del_tab_command(tabs.MucTab, 'plugintest')
def command_plugintest(self, args):
self.api.information("Command! With args {}".format(args))

View File

@ -1,77 +0,0 @@
"""
Display the time between two messages.
Helps you identify the times of a conversation. For example
if you disable the timestamps, and remove the join/quit notifications in a
chatroom, you cant really distinguish when a conversation stopped and when
a new one started, because you dont have a visual separation between the two.
This plugin displays a message in the conversation indicating the time that
passed between two messages, if the time is bigger than X minutes
(configurable, of course. Default is 15 minutes). This way you know how many time elapsed between
them, letting you understand more easily what is going on without any visual
clutter.
Configuration
-------------
You can configure the minimum delay between two messages, to display the time marker, in seconds. The default is 10 minutes (aka 600 seconds).
.. code-block:: ini
[time_marker]
delay = 600
Usage
-----
Messages like 2 hours, 25 minutes passed are automatically displayed into the converstation. You dont need to (and cant) do anything.
"""
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.jid.bare: last_message date, so we dont have to
# retrieve the messages of the given muc to look for the last
# messages date each time. Also, now that I think about it, the
# date of the message is not event kept in the Message object, so…
self.last_messages = {}
def on_muc_msg(self, message, tab):
def format_timedelta(delta):
"""
Return a string of the form D days, H hours, M minutes, S
seconds. If the number of total minutes is bigger than 10, we
usually dont care anymore about the number of seconds, so we
dont display it. Same thing if the number of days is bigger
than one, we dont display the minutes either.
"""
days = delta.days
hours = delta.seconds // 3600
minutes = delta.seconds // 60 % 60
seconds = delta.seconds % 60
res = ''
if days > 0:
res = "%s days, " % days
if hours > 0:
res += "%s hours, " % hours
if days == 0 and minutes != 0:
res += "%s minutes, " % minutes
if delta.total_seconds() < 600:
res += "%s seconds, " % seconds
return res[:-2]
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(
InfoMessage("%s passed…" % (format_timedelta(delta), ))
)

View File

@ -1,140 +0,0 @@
"""
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)

View File

@ -1,98 +0,0 @@
"""
Upload a file and auto-complete the input with its URL.
Usage
-----
This plugin adds a command to the chat tabs.
.. glossary::
/upload
**Usage:** ``/upload <filename>``
Uploads the <filename> file to the preferred HTTP File Upload
service (see XEP-0363) and fill the input with its URL.
"""
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 FileTooBig, HTTPError, UploadServiceNotFound
from poezio.plugin import BasePlugin
from poezio.core.structs import Completion
from poezio.decorators import command_args_parser
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')
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',
self.command_upload,
usage='<filename>',
help='Upload a file and auto-complete the input with its URL.',
short='Upload a file',
completion=self.completion_filename)
async def upload(self, filename, encrypted=False) -> Optional[str]:
try:
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 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 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):
if args is None:
self.core.command.help('upload')
return
filename, = args
filename = expanduser(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):
txt = expanduser(the_input.get_text()[8:])
files = glob(txt + '*')
return Completion(the_input.auto_completion, files, quotify=False)

View File

@ -1,49 +0,0 @@
"""
This plugin retrieves the uptime of a server.
Command
-------
.. glossary::
/uptime
**Usage:** ``/uptime <jid>``
Retrieve the uptime of the server of ``jid``.
"""
from poezio.plugin import BasePlugin
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):
def init(self):
self.api.add_command(
'uptime',
self.command_uptime,
usage='<jid>',
help='Ask for the uptime of a server or component (see XEP-0012).',
short='Get the uptime')
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(result.attrib['seconds']))), 'Info')
return
except (IqError, IqTimeout):
pass
self.api.information('Could not retrieve uptime', 'Error')

View File

@ -1,634 +0,0 @@
"""
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 dont want to receive the activity of your contacts.
enable_user_gaming
**Default value:** ``true``
Set this to ``false`` if you dont want to receive the gaming activity of your contacts.
enable_user_mood
**Default value:** ``true``
Set this to ``false`` if you dont 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',
}
}

View File

@ -1,323 +0,0 @@
"""
This plugin adds a :term:`/vcard` command to all tabs, allowing you to request
and display the vcard-temp of any given entity.
Command
-------
.. glossary::
/vcard
**Usage (globally):** ``/vcard <jid>``
**Usage (in a chatroom tab):** ``/vcard <jid or nick>``
**Usage (in a conversation or contact list tab):** ``/vcard [jid]``
Globally, you can do ``/vcard user@server.example`` to get a vcard.
In a chatroom , you can either do it on a JID or a nick (``/vcard nick``,
``/vcard room@muc.server.example/nick`` or ``/vcard
user@server.example``).
In a private or a direct conversation, you can do ``/vcard`` to request
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.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):
def init(self):
self.api.add_command(
'vcard',
self.command_vcard,
usage='<jid>',
help='Send an XMPP vcard request to jid (see XEP-0054).',
short='Send a vcard request',
completion=self.completion_vcard)
self.api.add_tab_command(
tabs.MucTab,
'vcard',
self.command_muc_vcard,
usage='<jid|nick>',
help='Send an XMPP vcard request to jid or nick (see XEP-0054).',
short='Send a vcard request.',
completion=self.completion_muc_vcard)
self.api.add_tab_command(
tabs.RosterInfoTab,
'vcard',
self.command_roster_vcard,
usage='<jid>',
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.DynamicConversationTab, tabs.StaticConversationTab):
self.api.add_tab_command(
_class,
'vcard',
self.command_private_vcard,
usage='[jid]',
help=
'Send an XMPP vcard request to the current interlocutor or the given JID.',
short='Send a vcard request',
completion=self.completion_vcard)
def _handle_vcard(self, iq):
'''Retrieves a vCard from vcard-temp and present it as a DataFormsTab.
'''
jid = iq['from']
if iq['type'] == 'error':
self.api.information(
'Error retrieving vCard for %s: %s: %s' %
(jid, iq['error']['type'], iq['error']['condition']), 'Error')
return
vcard = iq['vcard_temp']
form = self.core.xmpp['xep_0004'].make_form(
ftype='result', title='vCard of %s' % jid)
# TODO: implement the other fields.
form.add_field(
var='FN', ftype='text-single', label='Name', value=vcard['FN'])
form.add_field(
var='NICKNAME',
ftype='text-multi',
label='Nicknames',
value=vcard['NICKNAME'])
# TODO: find a way to detect whether this is present or not.
form.add_field(ftype='fixed', value='Full Name')
form.add_field(
var='N/GIVEN',
ftype='text-single',
label='Given',
value=vcard['N']['GIVEN'])
form.add_field(
var='N/MIDDLE',
ftype='text-single',
label='Middle',
value=vcard['N']['MIDDLE'])
form.add_field(
var='N/FAMILY',
ftype='text-single',
label='Family',
value=vcard['N']['FAMILY'])
form.add_field(
var='N/PREFIX',
ftype='text-single',
label='Prefix',
value=vcard['N']['PREFIX'])
form.add_field(
var='N/SUFFIX',
ftype='text-single',
label='Suffix',
value=vcard['N']['SUFFIX'])
for i, addr in enumerate(vcard['addresses']):
form.add_field(ftype='fixed', value='Address')
values = [type_ for type_ in addr.bool_interfaces if addr[type_]]
addr_type = form.add_field(
var='ADR %d/TYPE' % i,
ftype='list-multi',
label='Type',
value=values)
addr_type.add_option(label='Home', value='HOME')
for type_ in addr.bool_interfaces:
addr_type.add_option(label=type_, value=type_)
form.add_field(
var='ADR %d/POBOX' % i,
ftype='text-single',
label='Pobox',
value=addr['POBOX'])
form.add_field(
var='ADR %d/EXTADD' % i,
ftype='text-single',
label='Extended Address',
value=addr['EXTADD'])
form.add_field(
var='ADR %d/STREET' % i,
ftype='text-single',
label='Street',
value=addr['STREET'])
form.add_field(
var='ADR %d/LOCALITY' % i,
ftype='text-single',
label='Locality',
value=addr['LOCALITY'])
form.add_field(
var='ADR %d/REGION' % i,
ftype='text-single',
label='Region',
value=addr['REGION'])
form.add_field(
var='ADR %d/PCODE' % i,
ftype='text-single',
label='Post Code',
value=addr['PCODE'])
form.add_field(
var='ADR %d/CTRY' % i,
ftype='text-single',
label='Country',
value=addr['CTRY'])
for i, tel in enumerate(vcard['telephone_numbers']):
form.add_field(ftype='fixed', value='Telephone')
values = [type_ for type_ in tel.bool_interfaces if tel[type_]]
tel_type = form.add_field(
var='TEL %d/TYPE' % i,
ftype='list-multi',
label='Type',
value=values)
for type_ in tel.bool_interfaces:
tel_type.add_option(label=type_, value=type_)
form.add_field(
var='TEL %d/NUMBER' % i,
ftype='text-single',
label='Number',
value=tel['NUMBER'])
for i, email in enumerate(vcard['emails']):
form.add_field(ftype='fixed', value='Email address')
values = [type_ for type_ in email.bool_interfaces if email[type_]]
email_type = form.add_field(
var='EMAIL %d/TYPE' % i,
ftype='list-multi',
label='Type',
value=values)
for type_ in email.bool_interfaces:
email_type.add_option(label=type_, value=type_)
form.add_field(
var='EMAIL %d/USERID' % i,
ftype='text-single',
label='Email Address',
value=email['USERID'])
form.add_field(ftype='fixed', value='Misc')
form.add_field(
var='BDAY',
ftype='text-single',
label='Birthday',
value=str(vcard['BDAY']))
for i, jabberid in enumerate(vcard['jids']):
form.add_field(ftype='fixed', value='URL')
form.add_field(
var='JABBERID %d' % i,
ftype='jid-single',
label='URL',
value=jabberid['JABBERID'])
for i, url in enumerate(vcard['urls']):
form.add_field(ftype='fixed', value='URL')
form.add_field(
var='URL %d' % i,
ftype='text-single',
label='URL',
value=url['URL'])
for i, desc in enumerate(vcard['descriptions']):
form.add_field(ftype='fixed', value='Description')
form.add_field(
var='DESC %d' % i,
ftype='text-multi',
label='Description',
value=desc['DESC'])
on_validate = lambda form: self.core.close_tab()
on_cancel = lambda form: self.core.close_tab()
self.core.open_new_form(form, on_cancel, on_validate)
async def _get_vcard(self, jid):
'''Send an iq to ask the vCard.'''
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')
@command_args_parser.raw
def command_vcard(self, arg):
if not arg:
self.core.command.help('vcard')
return
try:
jid = JID(arg)
except InvalidJID:
self.api.information('Invalid JID: %s' % arg, 'Error')
return
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().jid.full)
@command_args_parser.raw
def command_muc_vcard(self, arg):
if not arg:
self.core.command.help('vcard')
return
user = self.api.current_tab().get_user_by_name(arg)
if user:
jid = self.api.current_tab().jid.bare + '/' + user.nick
else:
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):
if arg:
self.command_vcard(arg)
return
current = self.api.current_tab().selected_row
if isinstance(current, Resource):
asyncio.create_task(
self._get_vcard(JID(current.jid).bare)
)
elif isinstance(current, Contact):
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()]
return Completion(
the_input.auto_completion, contacts, '', quotify=False)
def completion_muc_vcard(self, the_input):
users = [user.nick for user in self.api.current_tab().users]
users.extend([
resource.jid for contact in roster.get_contacts()
for resource in contact.resources
])
return Completion(the_input.auto_completion, users, '', quotify=False)

View File

@ -1,29 +0,0 @@
"""
This plugin colors each character of a message in white.
Usage
-----
.. glossary::
/load white
Say something in a Chat tab.
.. note:: This plugin is best used when someone else is writing in black,
assuming everyone is using a white background. Black backgrounds matter too!
"""
from poezio.plugin import BasePlugin
from poezio import xhtml
class Plugin(BasePlugin):
def init(self):
self.api.add_event_handler('muc_say', self.whiteify)
self.api.add_event_handler('private_say', self.whiteify)
self.api.add_event_handler('conversation_say', self.whiteify)
@staticmethod
def whiteify(msg, _):
msg['body'] = '\x197}' + xhtml.clean_text(msg['body'])

View File

@ -0,0 +1,263 @@
#!/usr/bin/env python3
# From Nightfox theme
# Upstream: https://github.com/edeneast/nightfox.nvim
import poezio.theming
from datetime import datetime
NFOX_BLACK = "#393b44"
NFOX_RED = "#c94f6d"
NFOX_GREEN = "#81b29a"
NFOX_YELLOW = "#dbc074"
NFOX_BLUE = "#719cd6"
NFOX_MAGENTA = "#9d79d6"
NFOX_CYAN = "#63cdcf"
NFOX_WHITE = "#dfdfe0"
NFOX_ORANGE = "#f4a261"
NFOX_PINK = "#d67ad2"
NFOX_COMMENT = "#738091"
NFOX_BG0 = "#131a24"
NFOX_BG1 = "#192330"
NFOX_BG2 = "#212e3f"
NFOX_BG3 = "#29394f"
NFOX_BG4 = "#39506d"
NFOX_FG0 = "#d6d6d7"
NFOX_FG1 = "#cdcecf"
NFOX_FG2 = "#aeafb0"
NFOX_FG3 = "#71839b"
NFOX_SEL0 = "#2b3b51"
NFOX_SEL1 = "#3c5372"
# Redefine with 256 colors until poezio manages 24 bits
NFOX_BLACK = 59
NFOX_RED = 167
NFOX_GREEN = 108
NFOX_YELLOW = 180
NFOX_BLUE = 39
NFOX_MAGENTA = 140
NFOX_CYAN = 80
NFOX_WHITE = 188
NFOX_ORANGE = 215
NFOX_PINK = 176
NFOX_COMMENT = 102
NFOX_BG0 = 232
NFOX_BG1 = 234
NFOX_BG2 = 236
NFOX_BG3 = 237
NFOX_BG4 = 59
NFOX_FG0 = 253
NFOX_FG1 = 188
NFOX_FG2 = 145
NFOX_FG3 = 67
NFOX_SEL0 = 24
NFOX_SEL1 = 67
class NightfoxTheme(poezio.theming.Theme):
SHORT_TIME_FORMAT = '%H:%M'
SHORT_TIME_FORMAT_LENGTH = len(datetime.now().strftime(SHORT_TIME_FORMAT))
LONG_TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
LONG_TIME_FORMAT_LENGTH = len(datetime.now().strftime(LONG_TIME_FORMAT))
CHAR_TIME_LEFT = ''
CHAR_TIME_RIGHT = ''
CHAR_STATUS = ''
CHAR_CHATSTATE_ACTIVE = 'A'
CHAR_CHATSTATE_COMPOSING = ''
CHAR_CHATSTATE_PAUSED = 'p'
CHAR_AFFILIATION_OWNER = ''
CHAR_AFFILIATION_ADMIN = ''
CHAR_AFFILIATION_MEMBER = ''
CHAR_AFFILIATION_NONE = ''
CHAR_AFFILIATION_OUTCAST = '!'
CHAR_XML_IN = ''
CHAR_XML_OUT = ''
COLOR_OWN_NICK = (NFOX_CYAN, -1)
CHAR_BEFORE_NICK_ME = '@'
CHAR_AFTER_NICK_ME = ' '
CHAR_AFTER_NICK = ': '
CHAR_JOIN = ''
CHAR_QUIT = ''
CHAR_KICK = '-!-'
CHAR_NEW_TEXT_SEPARATOR = ''
CHAR_OK = ''
CHAR_ERROR = '!'
CHAR_EMPTY = ' '
CHAR_ACK_RECEIVED = CHAR_OK
CHAR_NACK = CHAR_ERROR
CHAR_COLUMN_ASC = ''
CHAR_COLUMN_DESC = ''
CHAR_ROSTER_ERROR = CHAR_ERROR
CHAR_ROSTER_TUNE = ''
CHAR_ROSTER_ASKED = '?'
CHAR_ROSTER_ACTIVITY = 'A'
CHAR_ROSTER_MOOD = 'M'
CHAR_ROSTER_GAMING = 'G'
CHAR_ROSTER_FROM = ''
CHAR_ROSTER_BOTH = ' '
CHAR_ROSTER_TO = ''
CHAR_ROSTER_NONE = '¬'
COLOR_INFORMATION_BAR = (NFOX_FG3, NFOX_BG1)
COLOR_WARNING_PROMPT = (NFOX_ORANGE, NFOX_BG1, 'b')
COLOR_STATUS_XA = (NFOX_FG0, NFOX_BG1)
COLOR_STATUS_AWAY = (NFOX_PINK, NFOX_BG1)
COLOR_STATUS_DND = (NFOX_RED, NFOX_BG1)
COLOR_STATUS_CHAT = (NFOX_BLUE, NFOX_BG1)
COLOR_STATUS_UNAVAILABLE = (NFOX_COMMENT, NFOX_BG1)
COLOR_STATUS_ONLINE = (NFOX_GREEN, NFOX_BG1)
COLOR_STATUS_NONE = (NFOX_FG1, NFOX_BG1)
COLOR_VERTICAL_SEPARATOR = (NFOX_COMMENT, -1)
COLOR_NEW_TEXT_SEPARATOR = (NFOX_COMMENT, -1)
COLOR_MORE_INDICATOR = (NFOX_MAGENTA, -1)
COLOR_TAB_NORMAL = (-1, NFOX_BG1)
COLOR_TAB_NONEMPTY = (-1, NFOX_BG3)
COLOR_TAB_JOINED = (NFOX_YELLOW, NFOX_BG1)
COLOR_TAB_CURRENT = (-1, NFOX_SEL0)
COLOR_TAB_COMPOSING = (NFOX_GREEN, NFOX_BG1)
COLOR_TAB_NEW_MESSAGE = (NFOX_ORANGE, NFOX_BG1)
COLOR_TAB_HIGHLIGHT = (NFOX_CYAN, NFOX_BG1)
COLOR_TAB_ATTENTION = (NFOX_RED, NFOX_BG1)
COLOR_TAB_PRIVATE = (NFOX_BLUE, NFOX_BG1)
COLOR_TAB_DISCONNECTED = (NFOX_COMMENT, NFOX_BG1)
COLOR_TAB_SCROLLED = (NFOX_PINK, NFOX_BG1)
COLOR_TOPIC_BAR = (-1, NFOX_BG1)
COLOR_SCROLLABLE_NUMBER = (NFOX_BLUE, -1, 'b')
COLOR_SELECTED_ROW = (-1, NFOX_SEL0)
COLOR_PRIVATE_NAME = (NFOX_BLUE, NFOX_BG1)
COLOR_CONVERSATION_NAME = (NFOX_GREEN, NFOX_BG1)
COLOR_CONVERSATION_RESOURCE = (NFOX_YELLOW, NFOX_BG1)
COLOR_GROUPCHAT_NAME = (NFOX_FG1, NFOX_BG1)
COLOR_COLUMN_HEADER = (NFOX_CYAN, NFOX_BG3)
COLOR_COLUMN_HEADER_SEL = (NFOX_CYAN, NFOX_SEL0)
COLOR_VERTICAL_TAB_NORMAL = (NFOX_FG2, -1)
COLOR_VERTICAL_TAB_NONEMPTY = (-1, NFOX_BG3)
COLOR_VERTICAL_TAB_JOINED = (NFOX_YELLOW, -1)
COLOR_VERTICAL_TAB_SCROLLED = (NFOX_PINK, -1)
COLOR_VERTICAL_TAB_CURRENT = (-1, NFOX_SEL0)
COLOR_VERTICAL_TAB_NEW_MESSAGE = (NFOX_ORANGE, -1)
COLOR_VERTICAL_TAB_COMPOSING = (NFOX_GREEN, -1)
COLOR_VERTICAL_TAB_HIGHLIGHT = (NFOX_CYAN, -1)
COLOR_VERTICAL_TAB_PRIVATE = (NFOX_BLUE, -1)
COLOR_VERTICAL_TAB_ATTENTION = (NFOX_RED, -1)
COLOR_VERTICAL_TAB_DISCONNECTED = (NFOX_COMMENT, -1)
COLOR_VERTICAL_TAB_NUMBER = (NFOX_BLUE, -1)
COLOR_INFORMATION_TEXT = (NFOX_BLUE, -1)
COLOR_NORMAL_TEXT = (NFOX_FG1, -1)
COLOR_WARNING_TEXT = (NFOX_ORANGE, -1)
COLOR_LOG_MSG = (NFOX_COMMENT, -1)
COLOR_ERROR_MSG = (NFOX_RED, NFOX_BG2, 'b')
COLOR_HELP_COMMANDS = (NFOX_GREEN, -1)
COLOR_HIGHLIGHT_NICK = "reverse"
COLOR_MUC_JID = (NFOX_BLUE, -1)
COLOR_USER_VISITOR = (NFOX_FG1, -1)
COLOR_USER_PARTICIPANT = (NFOX_FG2, -1)
COLOR_USER_NONE = (NFOX_COMMENT, -1)
COLOR_USER_MODERATOR = (NFOX_MAGENTA, -1)
COLOR_REMOTE_USER = (NFOX_ORANGE, -1)
COLOR_XML_IN = (NFOX_BLUE, -1)
COLOR_XML_OUT = (NFOX_GREEN, -1)
COLOR_ME_MESSAGE = (NFOX_CYAN, -1)
COLOR_REVISIONS_MESSAGE = (NFOX_MAGENTA, -1, 'b')
COLOR_IMPORTANT_TEXT = (NFOX_RED, -1, 'b')
COLOR_TIME_STRING = (NFOX_COMMENT, -1)
COLOR_CHAR_ACK = (NFOX_GREEN, -1)
COLOR_CHAR_NACK = (NFOX_ORANGE, -1)
COLOR_ROSTER_GAMING = (NFOX_CYAN, -1)
COLOR_ROSTER_MOOD = (NFOX_PINK, -1)
COLOR_ROSTER_ACTIVITY = (NFOX_GREEN, -1)
COLOR_ROSTER_TUNE = (NFOX_BLUE, -1)
COLOR_ROSTER_ERROR = (NFOX_RED, -1)
COLOR_ROSTER_SUBSCRIPTION = (NFOX_FG1, -1)
COLOR_JOIN_CHAR = (NFOX_FG2, -1)
COLOR_QUIT_CHAR = (NFOX_FG2, -1)
COLOR_KICK_CHAR = (NFOX_RED, -1)
MODE_TAB_NORMAL = ''
MODE_TAB_IMPORTANT = ''
MODE_TAB_NAME = ''
INFO_COLORS = {
'info': (NFOX_BLUE, -1),
'error': (NFOX_RED, 7, 'b'),
'warning': (NFOX_ORANGE, -1),
'roster': (NFOX_FG1, -1),
'help': (NFOX_GREEN, -1),
'headline': (NFOX_FG2, -1, 'b'),
'tune': (NFOX_BLUE, -1),
'gaming': (NFOX_CYAN, -1),
'mood': (NFOX_PINK, -1),
'activity': (NFOX_GREEN, -1),
'default': (NFOX_FG1, -1),
}
LIST_COLOR_NICKNAMES = [
(1, -1), (2, -1), (3, -1), (4, -1), (5, -1), (6, -1), (9, -1),
(10, -1), (11, -1), (12, -1), (13, -1), (14, -1), (19, -1),
(20, -1), (21, -1), (22, -1), (23, -1), (24, -1), (25, -1),
(26, -1), (27, -1), (28, -1), (29, -1), (30, -1), (31, -1),
(32, -1), (33, -1), (34, -1), (35, -1), (36, -1), (37, -1),
(38, -1), (39, -1), (40, -1), (41, -1), (42, -1), (43, -1),
(44, -1), (45, -1), (46, -1), (47, -1), (48, -1), (49, -1),
(50, -1), (51, -1), (54, -1), (55, -1), (56, -1), (57, -1),
(58, -1), (60, -1), (61, -1), (62, -1), (63, -1), (64, -1),
(65, -1), (66, -1), (67, -1), (68, -1), (69, -1), (70, -1),
(71, -1), (72, -1), (73, -1), (74, -1), (75, -1), (76, -1),
(77, -1), (78, -1), (79, -1), (81, -1), (82, -1),
(83, -1), (84, -1), (85, -1), (86, -1), (87, -1), (88, -1),
(89, -1), (90, -1), (91, -1), (92, -1), (93, -1), (94, -1),
(95, -1), (96, -1), (97, -1), (98, -1), (99, -1), (100, -1),
(101, -1), (103, -1), (104, -1), (105, -1), (106, -1), (107, -1),
(108, -1), (109, -1), (110, -1), (111, -1), (112, -1), (113, -1),
(114, -1), (115, -1), (116, -1), (117, -1), (118, -1), (119, -1),
(120, -1), (121, -1), (122, -1), (123, -1), (124, -1), (125, -1),
(126, -1), (127, -1), (128, -1), (129, -1), (130, -1), (131, -1),
(132, -1), (133, -1), (134, -1), (135, -1), (136, -1), (137, -1),
(138, -1), (139, -1), (140, -1), (141, -1), (142, -1), (143, -1),
(144, -1), (145, -1), (146, -1), (147, -1), (148, -1), (149, -1),
(150, -1), (151, -1), (152, -1), (153, -1), (154, -1), (155, -1),
(156, -1), (157, -1), (158, -1), (159, -1), (160, -1), (161, -1),
(162, -1), (163, -1), (164, -1), (165, -1), (166, -1), (167, -1),
(168, -1), (169, -1), (170, -1), (171, -1), (172, -1), (173, -1),
(174, -1), (175, -1), (176, -1), (177, -1), (178, -1), (179, -1),
(180, -1), (181, -1), (182, -1), (183, -1), (184, -1), (185, -1),
(186, -1), (187, -1), (188, -1), (189, -1), (190, -1), (191, -1),
(192, -1), (193, -1), (196, -1), (197, -1), (198, -1), (199, -1),
(200, -1), (201, -1), (202, -1), (203, -1), (204, -1), (205, -1),
(206, -1), (207, -1), (208, -1), (209, -1), (210, -1), (211, -1),
(212, -1), (213, -1), (214, -1), (215, -1), (216, -1), (217, -1),
(218, -1), (219, -1), (220, -1), (221, -1), (222, -1), (223, -1),
(224, -1), (225, -1), (226, -1), (227, -1)]
theme = NightfoxTheme()

View File

@ -16,11 +16,25 @@ if set --query _flag_help
exit
end
mkdir -p ~/.cache
if set --query _flag_desktop
# Copy the desktop part
echo "**** Copying desktop part"
rsync --exclude=".*.swp" -av ./desktop/ ~/
pip install --user --break-system-packages -U poezio-omemo epr-reader
rm -rf ~/.local/share/poezio/plugins
mkdir -p ~/.local/share/poezio
set -l poezio_version $(poezio --version | cut -d " " -f 2)
rm -rf ~/.cache/poezio-git
git clone --depth 1 https://codeberg.org/poezio/poezio ~/.cache/poezio-git
pushd ~/.cache/poezio-git
git checkout $poezio_version
popd
cp -r ~/.cache/poezio-git/plugins ~/.local/share/poezio
rm -rf ~/.cache/poezio-git
end
if set --query _flag_remove
@ -45,8 +59,7 @@ go install github.com/jesseduffield/lazydocker@latest
go install github.com/jesseduffield/lazygit@latest
rm -rf ~/.cache/nvimpager-git
mkdir -p ~/.cache
git clone https://github.com/lucc/nvimpager ~/.cache/nvimpager-git
git clone --depth 1 https://github.com/lucc/nvimpager ~/.cache/nvimpager-git
pushd ~/.cache/nvimpager-git
make PREFIX=$HOME/.local install
popd

View File

@ -28,7 +28,11 @@ if set --query _flag_private
echo "**** Copying private part"
rsync --exclude=".*.swp" -av ./private/ ~/
sed -i "s/REPLACEDEVICEHOSTNAME/`hostname`/g" ~/.config/poezio/poezio.cfg
sed -i "s/REPLACE_DEVICE_HOSTNAME/`hostname`/g" ~/.config/poezio/poezio.cfg
sed -i "s/REPLACE_FINGERPRINT/cert_fingerprint/g" ~/.config/poezio/poezio.cfg
sed -i "s/REPLACE_JID/template@example.com/g" ~/.config/poezio/poezio.cfg
sed -i "s/KEYRING_ID/xmpp:template@example.com/g" ~/.config/poezio/poezio.cfg
sed -i "s/KEYRING_USER/template@example.com/g" ~/.config/poezio/poezio.cfg
sed -i "s/FRESHRSSLOGIN/example_user/g" ~/.config/newsboat/config
sed -i "s/FRESHRSSKEEPASSXC/rss:freshrss.example.com/g" ~/.config/newsboat/config