Add a /sticker plugin

This plugin currently uploads the selected sticker every time, to the
HTTP File Upload service of the server (see XEP-0363), a future
optimisation would be to use XEP-0231 instead, for better caching on the
recipient side.

It relies on a helper tool to select the wanted sticker inside the pack,
a sample one is provided in tools/sticker-picker/, but it is not built
by default.
This commit is contained in:
Emmanuel Gil Peyrot 2022-02-09 22:47:38 +01:00
parent 9735b6d6dc
commit d35c0564b3
7 changed files with 325 additions and 0 deletions

View file

@ -1,4 +1,5 @@
recursive-include doc/source *
recursive-include tools
include data/poezio.1
include data/io.poez.Poezio.appdata.xml
include data/io.poez.Poezio.desktop

View file

@ -211,6 +211,11 @@ Plugin index
Adds convenient aliases to /status (/away, etc).
Sticker
:ref:`Documentation <sticker-plugin>`
Opens a graphical sticker picker and sends the selected one.
Tell
:ref:`Documentation <tell-plugin>`
@ -342,6 +347,7 @@ Plugin index
simple_notify
spam
status
sticker
tell
time_marker
uptime

View file

@ -0,0 +1,6 @@
.. _sticker-plugin:
Sticker
=======
.. automodule:: sticker

97
plugins/sticker.py Normal file
View file

@ -0,0 +1,97 @@
'''
This plugin lets the user select and send a sticker from a pack of stickers.
The protocol used here is based on XEP-0363 and XEP-0066, while a future
version may use XEP-0449 instead.
Command
-------
.. glossary::
/sticker
**Usage:** ``/sticker <pack>``
Opens a picker tool, and send the sticker which has been selected.
Configuration options
---------------------
.. glossary::
sticker_picker
**Default:** ``poezio-sticker-picker``
The command to invoke as a sticker picker. A sample one is provided in
tools/sticker-picker.
stickers_dir
**Default:** ``XDG_DATA_HOME/poezio/stickers``
The directory under which the sticker packs can be found.
'''
import asyncio
import concurrent.futures
from poezio import xdg
from poezio.plugin import BasePlugin
from poezio.config import config
from poezio.decorators import command_args_parser
from poezio.core.structs import Completion
from pathlib import Path
from asyncio.subprocess import PIPE, DEVNULL
class Plugin(BasePlugin):
dependencies = {'upload'}
def init(self):
# The command to use as a picker helper.
self.picker_command = config.getstr('sticker_picker') or 'poezio-sticker-picker'
# Select and create the stickers directory.
directory = config.getstr('stickers_dir')
if directory:
self.directory = Path(directory).expanduser()
else:
self.directory = xdg.DATA_HOME / 'stickers'
self.directory.mkdir(parents=True, exist_ok=True)
self.upload = self.refs['upload']
self.api.add_command('sticker', self.command_sticker,
usage='<sticker pack>',
short='Send a sticker',
help='Send a sticker, with a helper GUI sticker picker',
completion=self.completion_sticker)
def command_sticker(self, pack):
'''
Sends a sticker
'''
if not pack:
self.api.information('Missing sticker pack argument.', 'Error')
return
async def run_command(tab, path: Path):
try:
process = await asyncio.create_subprocess_exec(
self.picker_command, path, stdout=PIPE, stderr=PIPE)
sticker, stderr = await process.communicate()
except FileNotFoundError as err:
self.api.information('Failed to launch the sticker picker: %s' % err, 'Error')
return
else:
if process.returncode != 0:
self.api.information('Sticker picker failed: %s' % stderr.decode(), 'Error')
return
if sticker:
filename = sticker.decode().rstrip()
self.api.information('Sending sticker %s' % filename, 'Info')
await self.upload.send_upload(path / filename, tab)
tab = self.api.current_tab()
path = self.directory / pack
asyncio.create_task(run_command(tab, path))
def completion_sticker(self, the_input):
'''
Completion for /sticker
'''
txt = the_input.get_text()[9:]
directories = [directory.name for directory in self.directory.glob(txt + '*')]
return Completion(the_input.auto_completion, directories, quotify=False)

View file

@ -0,0 +1,16 @@
[package]
name = "poezio-sticker-picker"
version = "0.1.0"
edition = "2021"
authors = ["Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>"]
license = "Zlib"
description = "Helper tool for selecting a sticker inside a pack"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gtk = { package = "gtk4", version = "0.4", features = ["v4_6"] }
gdk = { package = "gdk4", version = "0.4", features = ["v4_6"] }
glib = "0.15"
gio = "0.15"
once_cell = "1.9.0"

View file

@ -0,0 +1,93 @@
// This file is part of Poezio.
//
// Poezio is free software: you can redistribute it and/or modify
// it under the terms of the zlib license. See the COPYING file.
mod sticker;
use gtk::prelude::*;
use sticker::StickerType as Sticker;
fn main() {
let app = gtk::Application::builder()
.application_id("io.poez.StickerPicker")
.flags(gio::ApplicationFlags::HANDLES_OPEN)
.build();
let quit = gio::SimpleAction::new("quit", None);
app.set_accels_for_action("app.quit", &["<Control>q"]);
app.add_action(&quit);
quit.connect_activate(glib::clone!(@weak app => move |_, _| app.quit()));
app.connect_open(move |app, directories, _| {
let path = match directories {
[directory] => directory.path().unwrap(),
_ => {
eprintln!("Only a single directory is allowed!");
std::process::exit(1);
}
};
let window = gtk::ApplicationWindow::builder()
.application(app)
.default_width(1280)
.default_height(720)
.title("Poezio Sticker Picker")
.build();
let sw = gtk::ScrolledWindow::builder()
.has_frame(true)
.hscrollbar_policy(gtk::PolicyType::Always)
.vscrollbar_policy(gtk::PolicyType::Always)
.vexpand(true)
.build();
window.set_child(Some(&sw));
let store = gio::ListStore::new(Sticker::static_type());
for dir_entry in std::fs::read_dir(path).unwrap() {
let dir_entry = dir_entry.unwrap();
let file_name = dir_entry.file_name().into_string().unwrap();
let sticker = Sticker::new(file_name, &dir_entry.path());
store.append(&sticker);
}
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(|_, item| {
let picture = gtk::Picture::builder()
.alternative_text("Sticker")
.can_shrink(false)
.build();
item.set_child(Some(&picture));
});
factory.connect_bind(|_, list_item| {
if let Some(child) = list_item.child() {
if let Some(item) = list_item.item() {
let picture: gtk::Picture = child.downcast().unwrap();
let sticker: Sticker = item.downcast().unwrap();
picture.set_paintable(sticker.texture().as_ref());
}
}
});
let selection = gtk::SingleSelection::new(Some(&store));
let grid_view = gtk::GridView::builder()
.single_click_activate(true)
.model(&selection)
.factory(&factory)
.build();
grid_view.connect_activate(move |_, position| {
let item = store.item(position).unwrap();
let sticker: Sticker = item.downcast().unwrap();
if let Some(filename) = sticker.filename() {
println!("{}", filename);
std::process::exit(0);
}
});
sw.set_child(Some(&grid_view));
window.show();
});
app.run();
}

View file

@ -0,0 +1,106 @@
// This file is part of Poezio.
//
// Poezio is free software: you can redistribute it and/or modify
// it under the terms of the zlib license. See the COPYING file.
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use std::cell::RefCell;
use std::path::Path;
#[derive(Debug, Default)]
pub struct Sticker {
filename: RefCell<Option<String>>,
texture: RefCell<Option<gdk::Texture>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Sticker {
const NAME: &'static str = "Sticker";
type Type = StickerType;
}
impl ObjectImpl for Sticker {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::new(
"filename",
"Filename",
"Filename",
None,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecObject::new(
"texture",
"Texture",
"Texture",
gdk::Texture::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
_obj: &StickerType,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"filename" => {
let filename = value.get().unwrap();
self.filename.replace(filename);
}
"texture" => {
let texture = value.get().unwrap();
self.texture.replace(texture);
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &StickerType, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"filename" => self.filename.borrow().to_value(),
"texture" => self.texture.borrow().to_value(),
_ => unimplemented!(),
}
}
}
glib::wrapper! {
pub struct StickerType(ObjectSubclass<Sticker>);
}
impl StickerType {
pub fn new(filename: String, path: &Path) -> StickerType {
let texture = gdk::Texture::from_filename(path).unwrap();
glib::Object::new(&[("filename", &filename), ("texture", &texture)])
.expect("Failed to create Sticker")
}
pub fn filename(&self) -> Option<String> {
let imp = self.imp();
let filename = imp.filename.borrow();
if let Some(filename) = filename.as_ref() {
Some(filename.clone())
} else {
None
}
}
pub fn texture(&self) -> Option<gdk::Texture> {
let imp = self.imp();
let texture = imp.texture.borrow();
if let Some(texture) = texture.as_ref() {
Some(texture.clone())
} else {
None
}
}
}