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:
parent
412a21f24c
commit
72d6f59e94
@ -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")
|
||||
|
@ -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
|
||||
|
||||
```
|
||||
|
@ -5,4 +5,6 @@
|
||||
|
||||
#define TEST_SOURCE_DIR "${CMAKE_SOURCE_DIR}/test"
|
||||
|
||||
#cmakedefine NoLottie
|
||||
|
||||
#endif
|
||||
|
205
client-utils.cpp
205
client-utils.cpp
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
323
sticker.cpp
Normal 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
28
sticker.h
Normal 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
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -5,6 +5,7 @@
|
||||
CommTest::CommTest()
|
||||
{
|
||||
tgprpl_set_test_backend(&tgl);
|
||||
tgprpl_set_single_thread();
|
||||
purple_init_plugin(&purplePlugin);
|
||||
purplePlugin.info->load(&purplePlugin);
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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
BIN
test/test.tgs
Normal file
Binary file not shown.
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user