2
0
mirror of https://github.com/ars3niy/tdlib-purple synced 2025-08-22 01:49:29 +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(NoWebp FALSE CACHE BOOL "Do not decode webp stickers")
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_HASH a3406de8d171bb422bb6ddf3bbd800e2 CACHE STRING "API hash")
set(STUFF "" CACHE STRING "")
@ -43,6 +44,7 @@ add_library(telegram-tdlib SHARED
${CMAKE_BINARY_DIR}/config.cpp
client-utils.cpp
format.cpp
sticker.cpp
)
include_directories(${Purple_INCLUDE_DIRS} ${CMAKE_BINARY_DIR})
@ -60,11 +62,15 @@ add_subdirectory(fmt)
target_compile_options(fmt PRIVATE -fPIC)
target_link_libraries(telegram-tdlib PRIVATE fmt::fmt)
if (NOT NoBundledLottie)
set(LOTTIE_MODULE OFF)
add_subdirectory(rlottie)
endif (NOT NoBundledLottie)
target_link_libraries(telegram-tdlib PRIVATE rlottie)
if (NOT NoLottie)
if (NOT NoBundledLottie)
set(LOTTIE_MODULE OFF)
add_subdirectory(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)
set(SHARE_INSTALL_PREFIX "share")

View File

@ -14,6 +14,11 @@ Missing features:
* Renaming groups/channels
* 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
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 without animated sticker decoding: `-DNoLottie`
## Proper user names in bitlbee
```

View File

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

View File

@ -6,12 +6,7 @@
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#include "buildopt.h"
#ifndef NoWebp
#include <png.h>
#include <webp/decode.h>
#endif
#include <functional>
enum {
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)
{
if (sendFailed.message_) {
@ -1498,3 +1341,49 @@ void populateGroupChatList(PurpleRoomlist *roomlist, const std::vector<const td:
}
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 "transceiver.h"
#include <purple.h>
#include <thread>
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,
const std::string &filePath, const std::string &fileDescription,
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 updateChatConversation(PurpleConvChat *purpleChat, const td::td_api::basicGroupFullInfo &groupInfo,
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,
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

View File

@ -727,15 +727,10 @@ struct GifWriter
// Creates a gif file.
// 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.
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
#if defined(_MSC_VER) && (_MSC_VER >= 1400)
writer->f = 0;
fopen_s(&writer->f, filename, "wb");
#else
writer->f = fopen(filename, "wb");
#endif
writer->f = fdopen(fd, "wb");
if(!writer->f) return false;
writer->firstFrame = true;

View File

@ -134,3 +134,12 @@ bool ignoreBigDownloads(PurpleAccount *account)
AccountOptions::BigDownloadHandlingDefault),
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_CHANNEL = 3;
class PurpleTdClient;
const char *getChatNameComponent();
GList *getChatJoinInfo();
std::string getPurpleChatName(const td::td_api::chat &chat);
@ -33,6 +35,8 @@ namespace AccountOptions {
constexpr const char *AcceptSecretChatsAlways = "always";
constexpr const char *AcceptSecretChatsNever = "never";
constexpr const char *AcceptSecretChatsDefault = AcceptSecretChatsAsk;
constexpr const char *AnimatedStickers = "animated-stickers";
constexpr gboolean AnimatedStickersDefault = TRUE;
};
namespace BuddyOptions {
@ -42,5 +46,6 @@ namespace BuddyOptions {
unsigned getAutoDownloadLimitKb(PurpleAccount *account);
bool isSizeWithinLimit(unsigned size, unsigned limit);
bool ignoreBigDownloads(PurpleAccount *account);
PurpleTdClient *getTdClient(PurpleAccount *account);
#endif

View File

@ -115,7 +115,6 @@ endif (NOT LIB_INSTALL_DIR)
#declare source and include files
add_subdirectory(inc)
add_subdirectory(src)
add_subdirectory(example)
if (LOTTIE_TEST)
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 "config.h"
#include "format.h"
#include "sticker.h"
#include <unistd.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,
const std::string &filePath, const char *caption,
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)) {
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)
text = "<img src=\"file://" + filePath + "\">";
else
@ -1071,28 +1077,41 @@ void PurpleTdClient::showStickerMessage(const td::td_api::chat &chat, TgMessageI
static bool isTgs(const std::string &path)
{
size_t dot = path.rfind('.');
if (dot != std::string::npos)
return !strcmp(path.c_str() + dot + 1, "tgs");
return false;
return (path.size() >= 4) && !strcmp(path.c_str() + path.size() - 4, ".tgs");
}
void PurpleTdClient::showDownloadedSticker(int64_t chatId, TgMessageInfo &message,
const std::string &filePath, const char *caption,
const std::string &fileDescription,
td::td_api::object_ptr<td::td_api::file> thumbnail)
{
if (isTgs(filePath) && 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);
#ifndef NoLottie
bool convertAnimated = !message.outgoing &&
purple_account_get_bool(m_account, AccountOptions::AnimatedStickers,
AccountOptions::AnimatedStickersDefault);
#else
bool convertAnimated = false;
#endif
if (isTgs(filePath)) {
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 {
const td::td_api::chat *chat = m_data.getChat(chatId);
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,
const std::string &filePath, const char *caption,

View File

@ -153,6 +153,7 @@ private:
const std::string &filePath, const char *caption,
const std::string &fileDescription,
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 uploadResponse(uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> object);

View File

@ -16,6 +16,10 @@
#include <ctype.h>
#include <unistd.h>
#ifndef NoLottie
#include <rlottie.h>
#endif
static char *_(const char *s) { return const_cast<char *>(s); }
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 "";
}
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)
{
PurpleTdClient *tdClient = getTdClient(purple_buddy_get_account(buddy));
@ -224,6 +219,11 @@ void tgprpl_set_test_backend(ITransceiverBackend *backend)
g_testBackend = backend;
}
void tgprpl_set_single_thread()
{
AccountThread::setSingleThread();
}
static void tgprpl_login (PurpleAccount *acct)
{
PurpleConnection *gc = purple_account_get_connection (acct);
@ -747,6 +747,10 @@ static void tgprpl_init (PurplePlugin *plugin)
PurpleTdClient::setLogLevel(1);
PurpleTdClient::setTdlibFatalErrorCallback(tdlibFatalErrorCallback);
#ifndef NoLottie
rlottie::configureModelCacheSize(0);
#endif
static_assert(AccountOptions::BigDownloadHandlingDefault == AccountOptions::BigDownloadHandlingAsk,
"default choice must be first");
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);
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);

View File

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

View File

@ -26,6 +26,7 @@ add_executable(tests EXCLUDE_FROM_ALL
${CMAKE_BINARY_DIR}/config.cpp
../client-utils.cpp
../format.cpp
../sticker.cpp
)
set_property(TARGET tests PROPERTY CXX_STANDARD 14)
@ -41,4 +42,11 @@ if (NOT NoWebp)
target_link_libraries(tests PRIVATE ${libwebp_LIBRARIES} ${libpng_LIBRARIES})
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)

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)
{
const int32_t date = 10001;

View File

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

View File

@ -1317,6 +1317,12 @@ PurpleAccountOption *purple_account_option_string_new(const char *text,
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,
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)
{
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 fileId[2] = {1234, 1235};
const int32_t thumbId = 1236;
purple_account_set_bool(account, "animated-stickers", FALSE);
loginWithOneContact();
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 "config.h"
#include "purple-info.h"
struct TimerCallbackData {
std::string accountUserName;
@ -17,12 +18,10 @@ static gboolean timerCallback(gpointer userdata)
PurpleAccount *account = purple_accounts_find(data->accountUserName.c_str(),
data->accountProtocolId.c_str());
PurpleConnection *connection = account ? purple_account_get_connection(account) : NULL;
void *protocolData = connection ? purple_connection_get_protocol_data(connection) : NULL;
PurpleTdClient *tdClient = account ? getTdClient(account) : nullptr;
if (protocolData) {
if (tdClient) {
// 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);
}