2
0
mirror of https://github.com/ars3niy/tdlib-purple synced 2025-08-22 09:57:52 +00:00

Show animated stickers as inline gifs

This commit is contained in:
Arseniy Lartsev 2020-06-13 16:01:05 +02:00
parent 412a21f24c
commit 72d6f59e94
22 changed files with 622 additions and 204 deletions

View File

@ -7,6 +7,7 @@ find_package(Td REQUIRED)
set(NoPkgConfig FALSE CACHE BOOL "Do not use pkg-config") set(NoPkgConfig FALSE CACHE BOOL "Do not use pkg-config")
set(NoWebp FALSE CACHE BOOL "Do not decode webp stickers") set(NoWebp FALSE CACHE BOOL "Do not decode webp stickers")
set(NoBundledLottie FALSE CACHE BOOL "Do not use bundled rlottie library") set(NoBundledLottie FALSE CACHE BOOL "Do not use bundled rlottie library")
set(NoLottie FALSE CACHE BOOL "Disable animated sticker conversion")
set(API_ID 94575 CACHE STRING "API id") set(API_ID 94575 CACHE STRING "API id")
set(API_HASH a3406de8d171bb422bb6ddf3bbd800e2 CACHE STRING "API hash") set(API_HASH a3406de8d171bb422bb6ddf3bbd800e2 CACHE STRING "API hash")
set(STUFF "" CACHE STRING "") set(STUFF "" CACHE STRING "")
@ -43,6 +44,7 @@ add_library(telegram-tdlib SHARED
${CMAKE_BINARY_DIR}/config.cpp ${CMAKE_BINARY_DIR}/config.cpp
client-utils.cpp client-utils.cpp
format.cpp format.cpp
sticker.cpp
) )
include_directories(${Purple_INCLUDE_DIRS} ${CMAKE_BINARY_DIR}) include_directories(${Purple_INCLUDE_DIRS} ${CMAKE_BINARY_DIR})
@ -60,11 +62,15 @@ add_subdirectory(fmt)
target_compile_options(fmt PRIVATE -fPIC) target_compile_options(fmt PRIVATE -fPIC)
target_link_libraries(telegram-tdlib PRIVATE fmt::fmt) target_link_libraries(telegram-tdlib PRIVATE fmt::fmt)
if (NOT NoBundledLottie) if (NOT NoLottie)
set(LOTTIE_MODULE OFF) if (NOT NoBundledLottie)
add_subdirectory(rlottie) set(LOTTIE_MODULE OFF)
endif (NOT NoBundledLottie) add_subdirectory(rlottie)
target_link_libraries(telegram-tdlib PRIVATE rlottie) target_compile_options(rlottie PRIVATE -fPIC)
target_include_directories(telegram-tdlib PRIVATE rlottie/inc)
endif (NOT NoBundledLottie)
target_link_libraries(telegram-tdlib PRIVATE rlottie)
endif (NOT NoLottie)
if (NOT DEFINED SHARE_INSTALL_PREFIX) if (NOT DEFINED SHARE_INSTALL_PREFIX)
set(SHARE_INSTALL_PREFIX "share") set(SHARE_INSTALL_PREFIX "share")

View File

@ -14,6 +14,11 @@ Missing features:
* Renaming groups/channels * Renaming groups/channels
* Secret chats * Secret chats
### Animated stickers
Converting animated stickers to GIFs is CPU-intensive. If this is a problem,
the conversion can be disabled in account settings, or even at compile time (see below).
## Installation ## Installation
RPM packages for Fedora and openSUSE, Debian and Ubuntu are available at https://download.opensuse.org/repositories/home:/ars3n1y/ . RPM packages for Fedora and openSUSE, Debian and Ubuntu are available at https://download.opensuse.org/repositories/home:/ars3n1y/ .
@ -61,6 +66,8 @@ To install, copy the .so to libpurple plugins directory, or run `make install`.
Building using existing librlottie: `-DNoBundledLottie` Building using existing librlottie: `-DNoBundledLottie`
Building without animated sticker decoding: `-DNoLottie`
## Proper user names in bitlbee ## Proper user names in bitlbee
``` ```

View File

@ -5,4 +5,6 @@
#define TEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/test" #define TEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/test"
#cmakedefine NoLottie
#endif #endif

View File

@ -6,12 +6,7 @@
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <algorithm> #include <algorithm>
#include <functional>
#include "buildopt.h"
#ifndef NoWebp
#include <png.h>
#include <webp/decode.h>
#endif
enum { enum {
FILE_UPLOAD_PRIORITY = 1, FILE_UPLOAD_PRIORITY = 1,
@ -1213,158 +1208,6 @@ void showGenericFile(const td::td_api::chat &chat, const TgMessageInfo &message,
} }
} }
#ifndef NoWebp
static void p2tgl_png_mem_write (png_structp png_ptr, png_bytep data, png_size_t length)
{
GByteArray *png_mem = (GByteArray *) png_get_io_ptr(png_ptr);
g_byte_array_append (png_mem, data, length);
}
int p2tgl_imgstore_add_with_id_png (const unsigned char *raw_bitmap, unsigned width, unsigned height)
{
GByteArray *png_mem = NULL;
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
png_bytepp rows = NULL;
// init png write struct
png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (png_ptr == NULL) {
purple_debug_misc(config::pluginId, "error encoding png (create_write_struct failed)\n");
return 0;
}
// init png info struct
info_ptr = png_create_info_struct (png_ptr);
if (info_ptr == NULL) {
png_destroy_write_struct(&png_ptr, NULL);
purple_debug_misc(config::pluginId, "error encoding png (create_info_struct failed)\n");
return 0;
}
// Set up error handling.
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr);
purple_debug_misc(config::pluginId, "error while writing png\n");
return 0;
}
// set img attributes
png_set_IHDR (png_ptr, info_ptr, width, height,
8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// alloc row pointers
rows = g_new0 (png_bytep, height);
if (rows == NULL) {
png_destroy_write_struct(&png_ptr, &info_ptr);
purple_debug_misc(config::pluginId, "error converting to png: malloc failed\n");
return 0;
}
unsigned i;
for (i = 0; i < height; i++)
rows[i] = (png_bytep)(raw_bitmap + i * width * 4);
// create array and set own png write function
png_mem = g_byte_array_new();
png_set_write_fn (png_ptr, png_mem, p2tgl_png_mem_write, NULL);
// write png
png_set_rows (png_ptr, info_ptr, rows);
png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
// cleanup
g_free(rows);
png_destroy_write_struct (&png_ptr, &info_ptr);
unsigned png_size = png_mem->len;
gpointer png_data = g_byte_array_free (png_mem, FALSE);
return purple_imgstore_add_with_id (png_data, png_size, NULL);
}
int p2tgl_imgstore_add_with_id_webp (const char *filename)
{
constexpr int MAX_W = 256;
constexpr int MAX_H = 256;
const uint8_t *data = NULL;
size_t len;
GError *err = NULL;
g_file_get_contents (filename, (gchar **) &data, &len, &err);
if (err) {
purple_debug_misc(config::pluginId, "cannot open file %s: %s\n", filename, err->message);
g_error_free(err);
return 0;
}
// downscale oversized sticker images displayed in chat, otherwise it would harm readabillity
WebPDecoderConfig config;
WebPInitDecoderConfig (&config);
if (WebPGetFeatures(data, len, &config.input) != VP8_STATUS_OK) {
purple_debug_misc(config::pluginId, "error reading webp bitstream: %s\n", filename);
g_free ((gchar *)data);
return 0;
}
config.options.use_scaling = 0;
config.options.scaled_width = config.input.width;
config.options.scaled_height = config.input.height;
if (config.options.scaled_width > MAX_W || config.options.scaled_height > MAX_H) {
const float max_scale_width = MAX_W * 1.0f / config.options.scaled_width;
const float max_scale_height = MAX_H * 1.0f / config.options.scaled_height;
if (max_scale_width < max_scale_height) {
// => the width is most limiting
config.options.scaled_width = MAX_W;
// Can't use ' *= ', because we need to do the multiplication in float
// (or double), and only THEN cast back to int.
config.options.scaled_height = (int) (config.options.scaled_height * max_scale_width);
} else {
// => the height is most limiting
config.options.scaled_height = MAX_H;
// Can't use ' *= ', because we need to do the multiplication in float
// (or double), and only THEN cast back to int.
config.options.scaled_width = (int) (config.options.scaled_width * max_scale_height);
}
config.options.use_scaling = 1;
}
config.output.colorspace = MODE_RGBA;
if (WebPDecode(data, len, &config) != VP8_STATUS_OK) {
purple_debug_misc(config::pluginId, "error decoding webp: %s\n", filename);
g_free ((gchar *)data);
return 0;
}
g_free ((gchar *)data);
const uint8_t *decoded = config.output.u.RGBA.rgba;
// convert and add
int imgStoreId = p2tgl_imgstore_add_with_id_png(decoded, config.options.scaled_width, config.options.scaled_height);
WebPFreeDecBuffer (&config.output);
return imgStoreId;
}
#else
int p2tgl_imgstore_add_with_id_webp (const char *filename)
{
return 0;
}
#endif
void showWebpSticker(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const std::string &fileDescription,
TdAccountData &account)
{
int id = p2tgl_imgstore_add_with_id_webp(filePath.c_str());
if (id != 0) {
std::string text = "\n<img id=\"" + std::to_string(id) + "\">";
showMessageText(account, chat, message, text.c_str(), NULL);
} else
showGenericFile(chat, message, filePath, fileDescription, account);
}
void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account) void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account)
{ {
if (sendFailed.message_) { if (sendFailed.message_) {
@ -1498,3 +1341,49 @@ void populateGroupChatList(PurpleRoomlist *roomlist, const std::vector<const td:
} }
purple_roomlist_set_in_progress(roomlist, FALSE); purple_roomlist_set_in_progress(roomlist, FALSE);
} }
AccountThread::AccountThread(PurpleAccount* purpleAccount, AccountThread::Callback callback)
{
m_accountUserName = purple_account_get_username(purpleAccount);
m_accountProtocolId = purple_account_get_protocol_id(purpleAccount);
m_callback = callback;
}
void AccountThread::threadFunc()
{
run();
g_idle_add(&AccountThread::mainThreadCallback, this);
}
static bool g_singleThread = false;
void AccountThread::setSingleThread()
{
g_singleThread = true;
}
void AccountThread::startThread()
{
if (!g_singleThread) {
if (!m_thread.joinable())
m_thread = std::thread(std::bind(&AccountThread::threadFunc, this));
} else {
run();
mainThreadCallback(this);
}
}
gboolean AccountThread::mainThreadCallback(gpointer data)
{
AccountThread *self = static_cast<AccountThread *>(data);
PurpleAccount *account = purple_accounts_find(self->m_accountUserName.c_str(),
self->m_accountProtocolId.c_str());
PurpleTdClient *tdClient = account ? getTdClient(account) : nullptr;
if (self->m_thread.joinable())
self->m_thread.join();
if (tdClient)
(tdClient->*(self->m_callback))(self);
return FALSE; // this idle callback will not be called again
}

View File

@ -4,6 +4,7 @@
#include "account-data.h" #include "account-data.h"
#include "transceiver.h" #include "transceiver.h"
#include <purple.h> #include <purple.h>
#include <thread>
const char *errorCodeMessage(); const char *errorCodeMessage();
@ -44,9 +45,6 @@ void showChatNotification(TdAccountData &account, const td::td_api::chat &chat,
void showGenericFile(const td::td_api::chat &chat, const TgMessageInfo &message, void showGenericFile(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const std::string &fileDescription, const std::string &filePath, const std::string &fileDescription,
TdAccountData &account); TdAccountData &account);
void showWebpSticker(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const std::string &fileDescription,
TdAccountData &account);
void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account); void notifySendFailed(const td::td_api::updateMessageSendFailed &sendFailed, TdAccountData &account);
void updateChatConversation(PurpleConvChat *purpleChat, const td::td_api::basicGroupFullInfo &groupInfo, void updateChatConversation(PurpleConvChat *purpleChat, const td::td_api::basicGroupFullInfo &groupInfo,
const TdAccountData &account); const TdAccountData &account);
@ -90,4 +88,24 @@ void updateOption(const td::td_api::updateOption &option, TdAccountData &account
void populateGroupChatList(PurpleRoomlist *roomlist, const std::vector<const td::td_api::chat *> &chats, void populateGroupChatList(PurpleRoomlist *roomlist, const std::vector<const td::td_api::chat *> &chats,
const TdAccountData &account); const TdAccountData &account);
class AccountThread {
public:
using Callback = void (PurpleTdClient::*)(AccountThread *thread);
static void setSingleThread();
AccountThread(PurpleAccount *purpleAccount, Callback callback);
virtual ~AccountThread() {}
void startThread();
private:
std::thread m_thread;
std::string m_accountUserName;
std::string m_accountProtocolId;
Callback m_callback;
void threadFunc();
static gboolean mainThreadCallback(gpointer data);
protected:
virtual void run() = 0;
};
#endif #endif

View File

@ -727,15 +727,10 @@ struct GifWriter
// Creates a gif file. // Creates a gif file.
// The input GIFWriter is assumed to be uninitialized. // The input GIFWriter is assumed to be uninitialized.
// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. // The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value.
bool GifBegin( GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false ) bool GifBegin( GifWriter* writer, int fd, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false )
{ {
(void)bitDepth; (void)dither; // Mute "Unused argument" warnings (void)bitDepth; (void)dither; // Mute "Unused argument" warnings
#if defined(_MSC_VER) && (_MSC_VER >= 1400) writer->f = fdopen(fd, "wb");
writer->f = 0;
fopen_s(&writer->f, filename, "wb");
#else
writer->f = fopen(filename, "wb");
#endif
if(!writer->f) return false; if(!writer->f) return false;
writer->firstFrame = true; writer->firstFrame = true;

View File

@ -134,3 +134,12 @@ bool ignoreBigDownloads(PurpleAccount *account)
AccountOptions::BigDownloadHandlingDefault), AccountOptions::BigDownloadHandlingDefault),
AccountOptions::BigDownloadHandlingDiscard); AccountOptions::BigDownloadHandlingDiscard);
} }
PurpleTdClient *getTdClient(PurpleAccount *account)
{
PurpleConnection *connection = purple_account_get_connection(account);
if (connection)
return static_cast<PurpleTdClient *>(purple_connection_get_protocol_data(connection));
else
return NULL;
}

View File

@ -9,6 +9,8 @@ static constexpr int
GROUP_TYPE_SUPER = 2, GROUP_TYPE_SUPER = 2,
GROUP_TYPE_CHANNEL = 3; GROUP_TYPE_CHANNEL = 3;
class PurpleTdClient;
const char *getChatNameComponent(); const char *getChatNameComponent();
GList *getChatJoinInfo(); GList *getChatJoinInfo();
std::string getPurpleChatName(const td::td_api::chat &chat); std::string getPurpleChatName(const td::td_api::chat &chat);
@ -33,6 +35,8 @@ namespace AccountOptions {
constexpr const char *AcceptSecretChatsAlways = "always"; constexpr const char *AcceptSecretChatsAlways = "always";
constexpr const char *AcceptSecretChatsNever = "never"; constexpr const char *AcceptSecretChatsNever = "never";
constexpr const char *AcceptSecretChatsDefault = AcceptSecretChatsAsk; constexpr const char *AcceptSecretChatsDefault = AcceptSecretChatsAsk;
constexpr const char *AnimatedStickers = "animated-stickers";
constexpr gboolean AnimatedStickersDefault = TRUE;
}; };
namespace BuddyOptions { namespace BuddyOptions {
@ -42,5 +46,6 @@ namespace BuddyOptions {
unsigned getAutoDownloadLimitKb(PurpleAccount *account); unsigned getAutoDownloadLimitKb(PurpleAccount *account);
bool isSizeWithinLimit(unsigned size, unsigned limit); bool isSizeWithinLimit(unsigned size, unsigned limit);
bool ignoreBigDownloads(PurpleAccount *account); bool ignoreBigDownloads(PurpleAccount *account);
PurpleTdClient *getTdClient(PurpleAccount *account);
#endif #endif

View File

@ -115,7 +115,6 @@ endif (NOT LIB_INSTALL_DIR)
#declare source and include files #declare source and include files
add_subdirectory(inc) add_subdirectory(inc)
add_subdirectory(src) add_subdirectory(src)
add_subdirectory(example)
if (LOTTIE_TEST) if (LOTTIE_TEST)
enable_testing() enable_testing()

323
sticker.cpp Normal file
View File

@ -0,0 +1,323 @@
#include "sticker.h"
#include "buildopt.h"
#include "config.h"
#include "gif.h"
#include "format.h"
#ifndef NoWebp
#include <png.h>
#include <webp/decode.h>
#endif
#ifndef NoLottie
#include <zlib.h>
#include <rlottie.h>
#endif
constexpr int MAX_W = 256;
constexpr int MAX_H = 256;
constexpr unsigned ANIMATED_WIDTH = 200;
constexpr unsigned ANIMATED_HEIGHT = 200;
#ifndef NoWebp
static void p2tgl_png_mem_write (png_structp png_ptr, png_bytep data, png_size_t length)
{
GByteArray *png_mem = (GByteArray *) png_get_io_ptr(png_ptr);
g_byte_array_append (png_mem, data, length);
}
int p2tgl_imgstore_add_with_id_png (const unsigned char *raw_bitmap, unsigned width, unsigned height)
{
GByteArray *png_mem = NULL;
png_structp png_ptr = NULL;
png_infop info_ptr = NULL;
png_bytepp rows = NULL;
// init png write struct
png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (png_ptr == NULL) {
purple_debug_misc(config::pluginId, "error encoding png (create_write_struct failed)\n");
return 0;
}
// init png info struct
info_ptr = png_create_info_struct (png_ptr);
if (info_ptr == NULL) {
png_destroy_write_struct(&png_ptr, NULL);
purple_debug_misc(config::pluginId, "error encoding png (create_info_struct failed)\n");
return 0;
}
// Set up error handling.
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr);
purple_debug_misc(config::pluginId, "error while writing png\n");
return 0;
}
// set img attributes
png_set_IHDR (png_ptr, info_ptr, width, height,
8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// alloc row pointers
rows = g_new0 (png_bytep, height);
if (rows == NULL) {
png_destroy_write_struct(&png_ptr, &info_ptr);
purple_debug_misc(config::pluginId, "error converting to png: malloc failed\n");
return 0;
}
unsigned i;
for (i = 0; i < height; i++)
rows[i] = (png_bytep)(raw_bitmap + i * width * 4);
// create array and set own png write function
png_mem = g_byte_array_new();
png_set_write_fn (png_ptr, png_mem, p2tgl_png_mem_write, NULL);
// write png
png_set_rows (png_ptr, info_ptr, rows);
png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
// cleanup
g_free(rows);
png_destroy_write_struct (&png_ptr, &info_ptr);
unsigned png_size = png_mem->len;
gpointer png_data = g_byte_array_free (png_mem, FALSE);
return purple_imgstore_add_with_id (png_data, png_size, NULL);
}
int p2tgl_imgstore_add_with_id_webp (const char *filename)
{
const uint8_t *data = NULL;
size_t len;
GError *err = NULL;
g_file_get_contents (filename, (gchar **) &data, &len, &err);
if (err) {
purple_debug_misc(config::pluginId, "cannot open file %s: %s\n", filename, err->message);
g_error_free(err);
return 0;
}
// downscale oversized sticker images displayed in chat, otherwise it would harm readabillity
WebPDecoderConfig config;
WebPInitDecoderConfig (&config);
if (WebPGetFeatures(data, len, &config.input) != VP8_STATUS_OK) {
purple_debug_misc(config::pluginId, "error reading webp bitstream: %s\n", filename);
g_free ((gchar *)data);
return 0;
}
config.options.use_scaling = 0;
config.options.scaled_width = config.input.width;
config.options.scaled_height = config.input.height;
if (config.options.scaled_width > MAX_W || config.options.scaled_height > MAX_H) {
const float max_scale_width = MAX_W * 1.0f / config.options.scaled_width;
const float max_scale_height = MAX_H * 1.0f / config.options.scaled_height;
if (max_scale_width < max_scale_height) {
// => the width is most limiting
config.options.scaled_width = MAX_W;
// Can't use ' *= ', because we need to do the multiplication in float
// (or double), and only THEN cast back to int.
config.options.scaled_height = (int) (config.options.scaled_height * max_scale_width);
} else {
// => the height is most limiting
config.options.scaled_height = MAX_H;
// Can't use ' *= ', because we need to do the multiplication in float
// (or double), and only THEN cast back to int.
config.options.scaled_width = (int) (config.options.scaled_width * max_scale_height);
}
config.options.use_scaling = 1;
}
config.output.colorspace = MODE_RGBA;
if (WebPDecode(data, len, &config) != VP8_STATUS_OK) {
purple_debug_misc(config::pluginId, "error decoding webp: %s\n", filename);
g_free ((gchar *)data);
return 0;
}
g_free ((gchar *)data);
const uint8_t *decoded = config.output.u.RGBA.rgba;
// convert and add
int imgStoreId = p2tgl_imgstore_add_with_id_png(decoded, config.options.scaled_width, config.options.scaled_height);
WebPFreeDecBuffer (&config.output);
return imgStoreId;
}
#else
int p2tgl_imgstore_add_with_id_webp (const char *filename)
{
return 0;
}
#endif
void showWebpSticker(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const std::string &fileDescription,
TdAccountData &account)
{
int id = p2tgl_imgstore_add_with_id_webp(filePath.c_str());
if (id != 0) {
std::string text = "\n<img id=\"" + std::to_string(id) + "\">";
showMessageText(account, chat, message, text.c_str(), NULL);
} else
showGenericFile(chat, message, filePath, fileDescription, account);
}
#ifndef NoLottie
static bool gunzip(gchar *compressedData, gsize compressedSize, std::string &output,
std::string &errorMessage)
{
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.avail_in = 0;
strm.next_in = Z_NULL;
int unzipResult = inflateInit2(&strm, MAX_WBITS + 16);
if (unzipResult != Z_OK) {
// Unlikely error message not worth translating
errorMessage = "Failed to initialize unzip stream";
return false;
}
if (compressedSize) {
char unzipBuffer[16384];
strm.avail_in = compressedSize;
strm.next_in = reinterpret_cast<uint8_t *>(compressedData);
do {
strm.avail_out = sizeof(unzipBuffer);
strm.next_out = reinterpret_cast<uint8_t *>(unzipBuffer);
unzipResult = inflate(&strm, Z_NO_FLUSH);
if ((unzipResult != Z_OK) && (unzipResult != Z_STREAM_END))
break;
if (strm.avail_out > sizeof(unzipBuffer)) {
unzipResult = Z_STREAM_ERROR;
break;
}
unsigned have = sizeof(unzipBuffer) - strm.avail_out;
output.append(unzipBuffer, have);
} while (strm.avail_out == 0);
}
(void)inflateEnd(&strm);
if ((unzipResult != Z_OK) && (unzipResult != Z_STREAM_END)) {
// Unlikely error message not worth translating
errorMessage = "Decompression error";
return false;
}
return true;
}
class GifBuilder {
public:
explicit GifBuilder(int fd, const uint32_t width,
const uint32_t height, const int bgColor=0xffffffff, const uint32_t delay = 2)
{
GifBegin(&handle, fd, width, height, delay);
bgColorR = (uint8_t) ((bgColor & 0xff0000) >> 16);
bgColorG = (uint8_t) ((bgColor & 0x00ff00) >> 8);
bgColorB = (uint8_t) ((bgColor & 0x0000ff));
}
~GifBuilder()
{
GifEnd(&handle);
}
void addFrame(rlottie::Surface &s, uint32_t delay = 2)
{
argbTorgba(s);
GifWriteFrame(&handle,
reinterpret_cast<uint8_t *>(s.buffer()),
s.width(),
s.height(),
delay);
}
void argbTorgba(rlottie::Surface &s)
{
uint8_t *buffer = reinterpret_cast<uint8_t *>(s.buffer());
uint32_t totalBytes = s.height() * s.bytesPerLine();
for (uint32_t i = 0; i < totalBytes; i += 4) {
unsigned char a = buffer[i+3];
// compute only if alpha is non zero
if (a) {
unsigned char r = buffer[i+2];
unsigned char b = buffer[i];
buffer[i] = r;
buffer[i+2] = b;
} else {
buffer[i+2] = bgColorB;
buffer[i+1] = bgColorG;
buffer[i] = bgColorR;
}
}
}
private:
GifWriter handle;
uint8_t bgColorR, bgColorG, bgColorB;
};
void StickerConversionThread::run()
{
gchar *compressedData = NULL;
gsize compressedSize = 0;
GError *error = NULL;
g_file_get_contents(inputFileName.c_str(), &compressedData, &compressedSize, &error);
if (error) {
m_errorMessage = error->message;
g_error_free(error);
return;
}
std::string lottieData;
bool gunzipSuccess = gunzip(compressedData, compressedSize, lottieData, m_errorMessage);
g_free(compressedData);
if (!gunzipSuccess)
return;
std::unique_ptr<rlottie::Animation> player = rlottie::Animation::loadFromData(lottieData, "");
if (!player) {
// Unlikely error message not worth translating
m_errorMessage = "Could not render animation";
return;
}
char *tempFileName = NULL;
int fd = g_file_open_tmp("tdlib_sticker_XXXXXX", &tempFileName, NULL);
if (fd < 0) {
// Unlikely error message not worth translating
m_errorMessage = "Could not create temporary file";
return;
}
m_outputFileName = tempFileName;
g_free(tempFileName);
unsigned w = ANIMATED_WIDTH;
unsigned h = ANIMATED_HEIGHT;
auto buffer = std::unique_ptr<uint32_t[]>(new uint32_t[w * h]);
size_t frameCount = player->totalFrame();
GifBuilder builder(fd, w, h, 0);
for (size_t i = 0; i < frameCount ; i++) {
rlottie::Surface surface(buffer.get(), w, h, w * 4);
player->renderSync(i, surface);
builder.addFrame(surface);
}
}
#else
void StickerConversionThread::run()
{
m_errorMessage = "Not supported";
}
#endif

28
sticker.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef _STICKER_H
#define _STICKER_H
#include "client-utils.h"
void showWebpSticker(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const std::string &fileDescription,
TdAccountData &account);
class StickerConversionThread: public AccountThread {
private:
std::string m_errorMessage;
std::string m_outputFileName;
void run() override;
public:
const std::string inputFileName;
const int64_t chatId;
const TgMessageInfo message;
StickerConversionThread(PurpleAccount *purpleAccount, Callback callback, const std::string &filename,
int64_t chatId, TgMessageInfo &&message)
: AccountThread(purpleAccount, callback), inputFileName(filename), chatId(chatId),
message(std::move(message)) {}
const std::string &getOutputFileName() const { return m_outputFileName; }
const std::string &getErrorMessage() const { return m_errorMessage; }
};
#endif

View File

@ -2,6 +2,7 @@
#include "purple-info.h" #include "purple-info.h"
#include "config.h" #include "config.h"
#include "format.h" #include "format.h"
#include "sticker.h"
#include <unistd.h> #include <unistd.h>
#include <stdlib.h> #include <stdlib.h>
@ -1008,6 +1009,11 @@ void PurpleTdClient::downloadResponse(uint64_t requestId, td::td_api::object_ptr
} }
} }
static std::string makeInlineImageText(int imgstoreId)
{
return "\n<img id=\"" + std::to_string(imgstoreId) + "\">";
}
void PurpleTdClient::showDownloadedImage(int64_t chatId, TgMessageInfo &message, void PurpleTdClient::showDownloadedImage(int64_t chatId, TgMessageInfo &message,
const std::string &filePath, const char *caption, const std::string &filePath, const char *caption,
const std::string &fileDesc, const std::string &fileDesc,
@ -1022,7 +1028,7 @@ void PurpleTdClient::showDownloadedImage(int64_t chatId, TgMessageInfo &message,
if (g_file_get_contents (filePath.c_str(), &data, &len, NULL)) { if (g_file_get_contents (filePath.c_str(), &data, &len, NULL)) {
int id = purple_imgstore_add_with_id (data, len, NULL); int id = purple_imgstore_add_with_id (data, len, NULL);
text = "\n<img id=\"" + std::to_string(id) + "\">"; text = makeInlineImageText(id);
} else if (filePath.find('"') == std::string::npos) } else if (filePath.find('"') == std::string::npos)
text = "<img src=\"file://" + filePath + "\">"; text = "<img src=\"file://" + filePath + "\">";
else else
@ -1071,28 +1077,41 @@ void PurpleTdClient::showStickerMessage(const td::td_api::chat &chat, TgMessageI
static bool isTgs(const std::string &path) static bool isTgs(const std::string &path)
{ {
size_t dot = path.rfind('.'); return (path.size() >= 4) && !strcmp(path.c_str() + path.size() - 4, ".tgs");
if (dot != std::string::npos)
return !strcmp(path.c_str() + dot + 1, "tgs");
return false;
} }
void PurpleTdClient::showDownloadedSticker(int64_t chatId, TgMessageInfo &message, void PurpleTdClient::showDownloadedSticker(int64_t chatId, TgMessageInfo &message,
const std::string &filePath, const char *caption, const std::string &filePath, const char *caption,
const std::string &fileDescription, const std::string &fileDescription,
td::td_api::object_ptr<td::td_api::file> thumbnail) td::td_api::object_ptr<td::td_api::file> thumbnail)
{ {
if (isTgs(filePath) && thumbnail) { #ifndef NoLottie
// Avoid message like "Downloading sticker thumbnail... bool convertAnimated = !message.outgoing &&
// Also ignore size limits, but only determined testers and crazy people would notice. purple_account_get_bool(m_account, AccountOptions::AnimatedStickers,
if (thumbnail->local_ && thumbnail->local_->is_downloading_completed_) AccountOptions::AnimatedStickersDefault);
showDownloadedSticker(chatId, message, thumbnail->local_->path_, caption, #else
fileDescription, nullptr); bool convertAnimated = false;
else #endif
downloadFile(thumbnail->id_, chatId, message, fileDescription, nullptr, if (isTgs(filePath)) {
&PurpleTdClient::showDownloadedSticker); if (convertAnimated) {
StickerConversionThread *thread;
thread = new StickerConversionThread(m_account, &PurpleTdClient::showConvertedAnimation,
filePath, chatId, std::move(message));
thread->startThread();
} else if (thumbnail) {
// Avoid message like "Downloading sticker thumbnail...
// Also ignore size limits, but only determined testers and crazy people would notice.
if (thumbnail->local_ && thumbnail->local_->is_downloading_completed_)
showDownloadedSticker(chatId, message, thumbnail->local_->path_, caption,
fileDescription, nullptr);
else
downloadFile(thumbnail->id_, chatId, message, fileDescription, nullptr,
&PurpleTdClient::showDownloadedSticker);
} else {
const td::td_api::chat *chat = m_data.getChat(chatId);
if (chat)
showGenericFile(*chat, message, filePath, fileDescription, m_data);
}
} else { } else {
const td::td_api::chat *chat = m_data.getChat(chatId); const td::td_api::chat *chat = m_data.getChat(chatId);
if (chat) if (chat)
@ -1100,6 +1119,42 @@ void PurpleTdClient::showDownloadedSticker(int64_t chatId, TgMessageInfo &messag
} }
} }
void PurpleTdClient::showConvertedAnimation(AccountThread *arg)
{
std::unique_ptr<AccountThread> baseThread(arg);
StickerConversionThread *thread = dynamic_cast<StickerConversionThread *>(arg);
const td::td_api::chat *chat = thread ? m_data.getChat(thread->chatId) : nullptr;
if (!chat || !thread)
return;
std::string errorMessage = thread->getErrorMessage();
gchar *imageData = NULL;
gsize imageSize = 0;
bool success = false;
if (errorMessage.empty()) {
GError *error = NULL;
g_file_get_contents(thread->getOutputFileName().c_str(), &imageData, &imageSize, &error);
if (error) {
// unlikely error message not worth translating
errorMessage = formatMessage("Could not read converted file {}: {}", {
thread->getOutputFileName(), error->message});
g_error_free(error);
} else
success = true;
remove(thread->getOutputFileName().c_str());
}
if (success) {
int id = purple_imgstore_add_with_id (imageData, imageSize, NULL);
std::string text = makeInlineImageText(id);
showMessageText(m_data, *chat, thread->message, text.c_str(), NULL);
} else {
errorMessage = formatMessage(_("Could not read sticker file {}: {}"),
{thread->inputFileName, errorMessage});
showMessageText(m_data, *chat, thread->message, NULL, errorMessage.c_str());
}
}
void PurpleTdClient::showDownloadedFile(int64_t chatId, TgMessageInfo &message, void PurpleTdClient::showDownloadedFile(int64_t chatId, TgMessageInfo &message,
const std::string &filePath, const char *caption, const std::string &filePath, const char *caption,

View File

@ -153,6 +153,7 @@ private:
const std::string &filePath, const char *caption, const std::string &filePath, const char *caption,
const std::string &fileDescription, const std::string &fileDescription,
td::td_api::object_ptr<td::td_api::file> thumbnail); td::td_api::object_ptr<td::td_api::file> thumbnail);
void showConvertedAnimation(AccountThread *arg);
void sendMessageCreatePrivateChatResponse(uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> object); void sendMessageCreatePrivateChatResponse(uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> object);
void uploadResponse(uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> object); void uploadResponse(uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> object);

View File

@ -16,6 +16,10 @@
#include <ctype.h> #include <ctype.h>
#include <unistd.h> #include <unistd.h>
#ifndef NoLottie
#include <rlottie.h>
#endif
static char *_(const char *s) { return const_cast<char *>(s); } static char *_(const char *s) { return const_cast<char *>(s); }
static const char *tgprpl_list_icon (PurpleAccount *acct, PurpleBuddy *buddy) static const char *tgprpl_list_icon (PurpleAccount *acct, PurpleBuddy *buddy)
@ -44,15 +48,6 @@ static const char *getLastOnline(const td::td_api::UserStatus &status)
return ""; return "";
} }
static PurpleTdClient *getTdClient(PurpleAccount *account)
{
PurpleConnection *connection = purple_account_get_connection(account);
if (connection)
return static_cast<PurpleTdClient *>(purple_connection_get_protocol_data(connection));
else
return NULL;
}
static void tgprpl_tooltip_text (PurpleBuddy *buddy, PurpleNotifyUserInfo *info, gboolean full) static void tgprpl_tooltip_text (PurpleBuddy *buddy, PurpleNotifyUserInfo *info, gboolean full)
{ {
PurpleTdClient *tdClient = getTdClient(purple_buddy_get_account(buddy)); PurpleTdClient *tdClient = getTdClient(purple_buddy_get_account(buddy));
@ -224,6 +219,11 @@ void tgprpl_set_test_backend(ITransceiverBackend *backend)
g_testBackend = backend; g_testBackend = backend;
} }
void tgprpl_set_single_thread()
{
AccountThread::setSingleThread();
}
static void tgprpl_login (PurpleAccount *acct) static void tgprpl_login (PurpleAccount *acct)
{ {
PurpleConnection *gc = purple_account_get_connection (acct); PurpleConnection *gc = purple_account_get_connection (acct);
@ -747,6 +747,10 @@ static void tgprpl_init (PurplePlugin *plugin)
PurpleTdClient::setLogLevel(1); PurpleTdClient::setLogLevel(1);
PurpleTdClient::setTdlibFatalErrorCallback(tdlibFatalErrorCallback); PurpleTdClient::setTdlibFatalErrorCallback(tdlibFatalErrorCallback);
#ifndef NoLottie
rlottie::configureModelCacheSize(0);
#endif
static_assert(AccountOptions::BigDownloadHandlingDefault == AccountOptions::BigDownloadHandlingAsk, static_assert(AccountOptions::BigDownloadHandlingDefault == AccountOptions::BigDownloadHandlingAsk,
"default choice must be first"); "default choice must be first");
GList *choices = NULL; GList *choices = NULL;
@ -770,6 +774,12 @@ static void tgprpl_init (PurplePlugin *plugin)
opt = purple_account_option_list_new (_("Accept secret chats"), AccountOptions::AcceptSecretChats, choices); opt = purple_account_option_list_new (_("Accept secret chats"), AccountOptions::AcceptSecretChats, choices);
prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, opt); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, opt);
#ifndef NoLottie
opt = purple_account_option_bool_new(_("Show animated stickers"), AccountOptions::AnimatedStickers,
AccountOptions::AnimatedStickersDefault);
prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, opt);
#endif
} }
static void setTwoFactorAuth(RequestData *data, PurpleRequestFields* fields); static void setTwoFactorAuth(RequestData *data, PurpleRequestFields* fields);

View File

@ -8,5 +8,6 @@ extern "C" {
gboolean purple_init_plugin(PurplePlugin *plugin); gboolean purple_init_plugin(PurplePlugin *plugin);
}; };
void tgprpl_set_test_backend(ITransceiverBackend *backend); void tgprpl_set_test_backend(ITransceiverBackend *backend);
void tgprpl_set_single_thread();
#endif #endif

View File

@ -26,6 +26,7 @@ add_executable(tests EXCLUDE_FROM_ALL
${CMAKE_BINARY_DIR}/config.cpp ${CMAKE_BINARY_DIR}/config.cpp
../client-utils.cpp ../client-utils.cpp
../format.cpp ../format.cpp
../sticker.cpp
) )
set_property(TARGET tests PROPERTY CXX_STANDARD 14) set_property(TARGET tests PROPERTY CXX_STANDARD 14)
@ -41,4 +42,11 @@ if (NOT NoWebp)
target_link_libraries(tests PRIVATE ${libwebp_LIBRARIES} ${libpng_LIBRARIES}) target_link_libraries(tests PRIVATE ${libwebp_LIBRARIES} ${libpng_LIBRARIES})
endif (NOT NoWebp) endif (NOT NoWebp)
if (NOT NoLottie)
if (NOT NoBundledLottie)
target_include_directories(tests PRIVATE ${CMAKE_SOURCE_DIR}/rlottie/inc)
endif (NOT NoBundledLottie)
target_link_libraries(tests PRIVATE rlottie)
endif (NOT NoLottie)
add_custom_target(run-tests ${CMAKE_CURRENT_BINARY_DIR}/tests DEPENDS tests) add_custom_target(run-tests ${CMAKE_CURRENT_BINARY_DIR}/tests DEPENDS tests)

View File

@ -353,6 +353,49 @@ TEST_F(FileTransferTest, DISABLED_WebpStickerDecode)
)); ));
} }
#ifndef NoLottie
TEST_F(FileTransferTest, AnimatedStickerDecode)
#else
TEST_F(FileTransferTest, DISABLED_AnimatedStickerDecode)
#endif
{
const int32_t date = 10001;
const int32_t fileId = 1234;
loginWithOneContact();
// No thumbnail, only .tgs
tgl.update(make_object<updateNewMessage>(makeMessage(
1,
userIds[0],
chatIds[0],
false,
date,
make_object<messageSticker>(make_object<sticker>(
0, 320, 200, "", true, false, nullptr,
nullptr,
make_object<file>(
fileId, 10000, 10000,
make_object<localFile>(TEST_SOURCE_DIR "/test.tgs", true, true, false, true, 0, 10000, 10000),
make_object<remoteFile>("beh", "bleh", false, true, 10000)
)
))
)));
tgl.verifyRequests({
make_object<viewMessages>(chatIds[0], std::vector<int64_t>(1, 1), true),
});
tgl.reply(make_object<ok>()); // reply to viewMessages
prpl.verifyEvents(ServGotImEvent(
connection,
purpleUserName(0),
// Sticker was converted to gif
"\n<img id=\"" + std::to_string(getLastImgstoreId()) + "\">",
PURPLE_MESSAGE_RECV,
date
));
}
TEST_F(FileTransferTest, Photo_DownloadProgress_StuckAtStart) TEST_F(FileTransferTest, Photo_DownloadProgress_StuckAtStart)
{ {
const int32_t date = 10001; const int32_t date = 10001;

View File

@ -5,6 +5,7 @@
CommTest::CommTest() CommTest::CommTest()
{ {
tgprpl_set_test_backend(&tgl); tgprpl_set_test_backend(&tgl);
tgprpl_set_single_thread();
purple_init_plugin(&purplePlugin); purple_init_plugin(&purplePlugin);
purplePlugin.info->load(&purplePlugin); purplePlugin.info->load(&purplePlugin);
} }

View File

@ -1317,6 +1317,12 @@ PurpleAccountOption *purple_account_option_string_new(const char *text,
return NULL; return NULL;
} }
PurpleAccountOption *purple_account_option_bool_new(const char *text,
const char *pref_name, gboolean default_value)
{
return NULL;
}
PurpleAccountOption *purple_account_option_list_new(const char *text, PurpleAccountOption *purple_account_option_list_new(const char *text,
const char *pref_name, GList *list) const char *pref_name, GList *list)
{ {
@ -1359,6 +1365,18 @@ void purple_account_set_string(PurpleAccount *account, const char *name,
} }
} }
gboolean purple_account_get_bool(const PurpleAccount *account, const char *name,
gboolean default_value)
{
return *purple_account_get_string(account, name, default_value ? "true" : "");
}
void purple_account_set_bool(PurpleAccount *account, const char *name,
gboolean value)
{
purple_account_set_string(account, name, value ? "true" : "");
}
char *purple_str_size_to_units(size_t size) char *purple_str_size_to_units(size_t size)
{ {
return g_strdup("purple_str_size_to_units"); return g_strdup("purple_str_size_to_units");

View File

@ -393,11 +393,12 @@ TEST_F(PrivateChatTest, Audio)
)); ));
} }
TEST_F(PrivateChatTest, Sticker) TEST_F(PrivateChatTest, Sticker_AnimatedDisabled)
{ {
const int32_t date = 10001; const int32_t date = 10001;
const int32_t fileId[2] = {1234, 1235}; const int32_t fileId[2] = {1234, 1235};
const int32_t thumbId = 1236; const int32_t thumbId = 1236;
purple_account_set_bool(account, "animated-stickers", FALSE);
loginWithOneContact(); loginWithOneContact();
tgl.update(make_object<updateNewMessage>(makeMessage( tgl.update(make_object<updateNewMessage>(makeMessage(

BIN
test/test.tgs Normal file

Binary file not shown.

View File

@ -1,5 +1,6 @@
#include "transceiver.h" #include "transceiver.h"
#include "config.h" #include "config.h"
#include "purple-info.h"
struct TimerCallbackData { struct TimerCallbackData {
std::string accountUserName; std::string accountUserName;
@ -17,12 +18,10 @@ static gboolean timerCallback(gpointer userdata)
PurpleAccount *account = purple_accounts_find(data->accountUserName.c_str(), PurpleAccount *account = purple_accounts_find(data->accountUserName.c_str(),
data->accountProtocolId.c_str()); data->accountProtocolId.c_str());
PurpleConnection *connection = account ? purple_account_get_connection(account) : NULL; PurpleTdClient *tdClient = account ? getTdClient(account) : nullptr;
void *protocolData = connection ? purple_connection_get_protocol_data(connection) : NULL;
if (protocolData) { if (tdClient) {
// If this is somehow not our PurpleTdClient then user really has themselves to blame // If this is somehow not our PurpleTdClient then user really has themselves to blame
PurpleTdClient *tdClient = static_cast<PurpleTdClient *>(protocolData);
(tdClient->*(data->callback))(data->requestId, nullptr); (tdClient->*(data->callback))(data->requestId, nullptr);
} }