Commit d7e58ec1 authored by Alexander's avatar Alexander

[stickers] Implement the manual download of sticker packs

parent 06a13409
Pipeline #6870 failed with stage
......@@ -43,3 +43,4 @@ STICKERS_NAMESPACE = 'urn:xmpp:stickers:0'
SFS_NAMESPACE = 'urn:xmpp:sfs:0'
FME_NAMESPACE = 'urn:xmpp:file:metadata:0'
......@@ -22,8 +22,8 @@ from gi.repository import GObject
from gajim.common.i18n import _
from gajim.plugins.gui import GajimPluginConfigDialog
from gajim.plugins.helpers import get_builder
from gajim.gtk.dialogs import ConfirmationDialog
from gajim.gtk.dialogs import DialogButton
from gajim.gui.dialogs import ConfirmationDialog
from gajim.gui.dialogs import DialogButton
class StickerPackObject(GObject.GObject):
def __init__(self, name, summary, amount, id_):
name: Stickers
short_name: stickers
version: 1.1.0
version: 1.2.0
description: Send stickers. Note that stickers are currently always sent unencrypted. Requires python-pillow.
authors = Alexander "PapaTutuWawa" <>
homepage =
homepage =
min_gajim_version: 1.2.91
max_gajim_version: 1.3.90
......@@ -35,6 +35,7 @@ from stickers.common import STICKERS_NAMESPACE
from stickers.common import SFS_NAMESPACE
from stickers.common import FME_NAMESPACE
from stickers.common import URL_DATA_NAMESPACE
from stickers.common import PUBSUB_MAX_FEATURE
log = logging.getLogger('gajim.p.stickers.module')
......@@ -96,6 +97,7 @@ class StickersModule(BaseModule):
self._plugin = find_one(lambda x: x.short_name == 'stickers', app.plugin_manager.plugins)
self._pack_upload_state = {} # Sticker pack ID -> (filename -> uploaded?)
self._account_pubsub_max = {} # Account name -> Max PubSub items
def _sticker_packs(self):
......@@ -257,8 +259,7 @@ class StickersModule(BaseModule):
node_options = {
'pubsub#persist_items': 'true',
# TODO: Spec says 'max' could be used but my server does not suport this
'pubsub#max_items': '10',
'pubsub#max_items': self._account_pubsub_max[self._client.account],
'pubsub#access_model': 'presence'
......@@ -290,6 +291,23 @@ class StickersModule(BaseModule):
uri = transfer.get_transformed_uri()
self._on_sticker_uploaded(transfer.sticker, transfer.pack_id, uri)
def _discover_pubsub_max(self):
Find the maximum amount of persistent items in a PubSub node out.
features = self._client.get_module('Discovery').server_info.features
if PUBSUB_MAX_FEATURE in features:
log.debug('%s supports pubsub#max_items=max', self._client.account)
self._account_pubsub_max[self._client.account] = 'max'
# NOTE: We could do some fancy things like discover the default
# PubSub configuration and find out what 'max' would default
# to, but that is more complicated.
# As such, we just default to a reasonable guess of how many
# sticker packs a user might use.
log.debug('%s does not support pubsub#max_items=max', self._client.account)
self._account_pubsub_max[self._client.account] = '50'
def publish_pack(self, sticker_pack, upload=True):
Publish a sticker pack on PubSub
......@@ -297,6 +315,10 @@ class StickersModule(BaseModule):
sticker_pack: The sticker pack object that should be published
upload: Should the stickers be first uploaded using HTTP Upload
# Check if we have a PubSub max for the account
if self._client.account not in self._account_pubsub_max:
if not upload:
......@@ -43,6 +43,8 @@ from gajim.groupchat_control import GroupchatControl
from gajim.common import app
from gajim.common import ged
from gajim.common.structs import OutgoingMessage
from gajim.gui.dialogs import ConfirmationDialog
from gajim.gui.dialogs import DialogButton
from gajim.plugins import GajimPlugin
from gajim.plugins.helpers import get_builder
......@@ -62,6 +64,7 @@ from stickers.utils import dict_append_or_set
from stickers.utils import hash_function_from_algo
from stickers.utils import body
from stickers.utils import detect_path_escape
from stickers.utils import print_widget
from stickers.common import Sticker
from stickers.common import Hash
from stickers.common import StickerPack
......@@ -344,8 +347,7 @@ class StickersPlugin(GajimPlugin):
for mention in self.sticker_mentions[pack.id_]:
sticker = find_one(lambda x: x.desc == mention.desc, pack.stickers)
buf = mention.start.get_buffer()
......@@ -440,7 +442,12 @@ class StickersPlugin(GajimPlugin):
# Download the stickers
# NOTE: Maybe some stickers do not have a URL, so we don't even bother with them
downloadable_stickers = filter(lambda x: x.url, event.pack.stickers)
downloadable_stickers = list(filter(lambda x: x.url, event.pack.stickers))
if not downloadable_stickers:
log.error('No downloadable stickers found for %s', event.pack.id_)
log.debug('Downloading stickers of stickerpack %s', event.pack.id_)
for i, sticker in enumerate(downloadable_stickers):
msg ='GET', sticker.url)
......@@ -493,20 +500,22 @@ class StickersPlugin(GajimPlugin):
fs = event.stanza.getTag('file-sharing')
url = fs.getTag('sources').getTag('url-data').getAttr('target')
desc = body(fs.getTag('file'), 'desc')
jid = event.stanza.getAttr('from').bare
event.additional_data['sticker'] = {
'pack_id': pack_id,
'url': url,
'desc': desc,
'account': event.account,
'from': jid,
# Download the sticker pack if we don't have it
if (not self.__has_sticker_pack(pack_id) and
jid = event.stanza.getAttr('from')
def _sticker_width(self):
......@@ -532,17 +541,66 @@ class StickersPlugin(GajimPlugin):
end_mark = buffer_.create_mark(None, iter_, True)
return start_mark, end_mark
def _update_textview(self, buf, iter_, tv, sticker, end=None):
def _on_manual_sticker_download(self, pack_id, jid, account):
def download_sticker_pack():
log.debug('Manually requested download of sticker pack %s from %s',
if pack_id in self.sticker_requests:
_('Download Sticker Pack'),
_('Are you sure you want to do download this sticker pack?'),
_('This will create files on your computer and publish it on your account.'),
def _print_non_download_mention(self, iter_, tv, pack_id, jid, account, text):
Print a button to allow manual downloading of a sticker pack. Returns
the start and end marks of the button.
iter_: The TextView's iterator
tv: The TextView
pack_id: The ID of the sticker pack
jid: The bare JID of the sender
account: The name of the account that received the message
text: The text of the message
button = Gtk.Button.new_with_label(text)
button.connect('clicked', lambda x: self._on_manual_sticker_download(pack_id, jid, account))
button.set_tooltip_text('Download this sticker pack')
start_mark, end_mark = print_widget(button, iter_, tv, True)
if tv.autoscroll:
return start_mark, end_mark
def _print_sticker(self, iter_, tv, sticker, end=None):
Print a sticker.
iter_: The TextView's iterator
tv: The TextView
sticker: The Sticker object
end: If end is a TextMark, remove everything from iter_ to the end mark.
img = image_from_pixbuf(sticker.pixbuf)
anchor = buf.create_child_anchor(iter_)
anchor.plaintext = ''
img.show_all(), anchor)
tv.plugin_modified = True
print_widget(img, iter_, tv, False)
if end:
buf =
buf.delete(iter_, buf.get_iter_at_mark(end))
if tv.autoscroll:
......@@ -555,8 +613,18 @@ class StickersPlugin(GajimPlugin):
# Find the correct sticker pack
pack_id = additional_data['sticker']['pack_id']
if not self.__has_sticker_pack(pack_id):
if not self._should_download_sticker_pack():
from_ = additional_data['sticker']['from']
account = additional_data['sticker']['account']
start, end = self._print_non_download_mention(iter_,
# We have requested it during _on_message_received
start, end = self._print_text(,
start, end = self._print_text(tv.get_buffer(),
......@@ -580,8 +648,7 @@ class StickersPlugin(GajimPlugin):
log.warning('Sticker "%s" of "%s" not found!', text, pack_id)
import sys
import os
import json
import hashlib
import mimetypes
from PIL import Image
config_path = sys.argv[1]
with open(config_path, 'r') as f:
config = json.loads(
pack_path = os.path.dirname(config_path)
unit_sep = str(0x1f)
record_sep = str(0x1e)
group_sep = str(0x1d)
file_sep = str(0x1c)
# TODO: Handle multiple languages
meta_string = f'{config["name"]}{unit_sep}{config["summary"]}{record_sep}{file_sep}'
tmp = ''
for sticker in config['stickers']:
tmp += sticker['desc'] + record_sep
sticker_file = os.path.join(pack_path, sticker['filename'])
with open(sticker_file, 'rb') as f:
sha256_hash = hashlib.sha256(
sticker['hashes'] = [{'algo': 'sha-256', 'value': sha256_hash}]
tmp += 'sha-256' + sha256_hash + unit_sep + record_sep
tmp += group_sep
image =
sticker['dimension'] = f'{image.width}x{image.height}'
sticker['size'] = os.path.getsize(sticker_file)
sticker['type'] = mimetypes.MimeTypes().guess_type(sticker_file)[0]
tmp += file_sep
result = meta_string + tmp
config['hash'] = {'algo': 'sha-256', 'value': hashlib.sha256(result.encode()).hexdigest()}
config['id'] = config['hash']['value'][:24]
with open(config_path, 'w') as f:
f.write(json.dumps(config, indent=4))
print('Sticker pack configuration updated!')
print(f'New sticker pack ID: {config["id"]}')
......@@ -82,3 +82,33 @@ def detect_path_escape(spath, full_path):
real_path = os.path.realpath(full_path)
return not real_path.startswith(spath)
def print_widget(widget, iter_, tv, create_marks=False):
"Print" a widget into the TextView
widget: The Gtk.Widget to print
buf: The TextView's buffer
iter_: The iterator
tv: The actual TextView
create_marks: Whether Gtk.TextMarks should be created before and after
the widget insertion. If True, the start and end mark are
returned. If False, None, None is returned.
buf =
if create_marks:
start_mark = buf.create_mark(None, iter_, True)
start_mark = None
anchor = buf.create_child_anchor(iter_)
anchor.plaintext = '', anchor)
tv.plugin_modified = True
if create_marks:
end_mark = buf.create_mark(None, iter_, True)
end_mark = None
return start_mark, end_mark
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment